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>
The XP flavor keeps the full runtime AND the mapping editor (decided:
editor everywhere). Gated per flavor:
- Wallpaper maker + WallpaperCanvas + generation (SkiaSharp) are
net48-only; the XP coordinator applies the profile's pre-rendered
wallpaper instead, and WallpaperApplier converts PNG->BMP on net40
(XP SPI_SETDESKWALLPAPER accepts only BMP).
- Joystick chain: ViGEm branch compiled out on net40 (HID feeder stays,
ready for RioGamepadXP.sys); the editor button picker offers the full
96 HID buttons on net40 vs the 11 named Xbox buttons on net48.
Verified: net40/x86 exe boots on Win10; the editor renders offline in a
32-bit host with a Button-50 joystick binding resolving correctly and
live gauges working (screenshot harness).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TargetFrameworks net48;net40. net48 keeps x64 + ViGEm + System.IO.Ports
package; net40 adds Microsoft.Bcl.Async + System.ValueTuple and uses the
in-box SerialPort.
- Compat/TaskCompat bridges Task.Run/Delay/WhenAny/WhenAll (TaskEx on
net40) and SemaphoreSlim.WaitAsync (net40 blocks briefly - trivial at
9600 baud).
- IReadOnlyList/IReadOnlyDictionary -> IList/IDictionary throughout
(net40 predates the IReadOnly* interfaces and the Bcl backport cannot
make arrays implement them).
- HashCode.Combine replaced with a manual combine (Bcl.HashCode has no
net40 build); Marshal.SizeOf<T> -> typeof form; ViGEmJoystickSink
gated #if !NET40. HidFeederJoystickSink stays on both flavors - it
will drive RioGamepadXP.sys on XP via the same contract.
Both TFMs build; 275 tests green on net48.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prepares RioJoy.Core for the net40 (Windows XP) target, which has no
System.Memory, ValueTask, or System.Text.Json:
- IRioTransport and the whole protocol/framing layer now use byte[] +
Task (RioPacket.Payload, PacketParser/Builder, RioChecksum, replies,
AnalogReport, RioHidReport). At 9600 baud Span bought nothing; the
SerialPortTransport bridge copies disappear entirely.
- ConfigStore/OverlayTemplateStore switch to Newtonsoft 13 with the
same conventions (indented, PascalCase, string enums, null-skipping);
verified against the real STJ-written config.json and regions.json
(load + round-trip). System.Memory and System.Text.Json packages
dropped.
275 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The universal archive gains a root README.txt aimed at standalone
installers (which entry point to run, supported OSes, offline prereqs).
postinstall.bat (the TeslaConsole hook) deletes install.bat and that
README as its final step so cabinet deploys leave C:\games clean;
install.bat keeps the README as the standalone machine's docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Mapping editor ships in all instances, XP included (wallpaper maker
stays modern-only with SkiaSharp).
- One dist zip deploys on both XP and Win10/11: app (net48 x64) +
app-xp (net40 x86) + vendor prereqs incl. offline .NET 4.0/KB2468871
redistributables and RioGamepadXP.sys.
- Two entry points: postinstall.bat (TeslaConsole/launcher, unattended)
and install.bat (freestanding, adds shortcuts). OS-detected via ver;
pure cmd on the XP path.
All Phase 8 open decisions are now resolved; ready to implement 8A.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vJoy/PPJoy are unmaintained, so third-party virtual joysticks are out.
The XP joystick becomes a thin WDM HID minidriver (vhidmini shape,
WDK 7.1.0, x86, no signing required on XP) exposing the same Public.h
IOCTL + 25-byte report contract as the modern RioGamepad.sys, so
HidFeederJoystickSink drives both. Precedent: FASA's tasgame.sys was an
XP HID minidriver. Staged: XP app ships keyboard/lamps/plasma first,
driver as milestone 2. Win10/11 chain unchanged (ViGEm preferred).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One codebase, two flavors: Win10/11 stays net48/x64 with ViGEm/RioGamepad
and remains the authoring platform; XP SP3 gets a net40/x86 runtime with
vJoy (full 96-button fidelity), pre-rendered wallpapers (PNG converted to
BMP at apply), Newtonsoft JSON shared by both flavors, Bcl.Async + small
compat shims, and a .bat-only installer. Grounded in a source audit:
about 20 Span/Memory uses in the 9600-baud serial path, 6 modern Task
calls, 1 HashCode.Combine, System.Text.Json in two stores.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The editor's L/R gauges read Rx/Ry, but EnableZR (on in real profiles)
folds the pedals into Rz and pins Rx/Ry to center, so the bars never
moved with the physical pedals. AxisCalibrator now exposes
LeftPedalOutput/RightPedalOutput (the calibrated pre-mix positions) and
RioRuntime.AxesUpdated carries an AxisReadout (the six virtual axes +
both pedals); the strip draws L/R from the pedal readouts, Z/Rz/X/Y
from the axes. With ZR off the readouts equal Rx/Ry, so nothing
changes there. Verified by screenshot: Rx/Ry centered while L=75% and
R=25% render correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closing the profile editor froze the tray app: the FormClosed handler
tears down the coordinator on the UI thread, and net48's
SerialStream.Dispose calls FlushFileBuffers, which blocks until the
driver drains buffered TX — indefinitely when the peer has stopped
reading (a wedged board, or a virtual-port pair with no reader). The
55 ms analog polls guarantee a TX backlog against such a peer.
Diagnosed from a live hang: the UI thread was parked in
NtFlushBuffersFile under SerialPortTransport.Dispose.
SerialPortTransport.Dispose now aborts/clears both queues first (so the
flush has nothing to wait on) and runs the close on the pool with a 2 s
grace period, so no driver can hang the calling thread. Verified against
the same wedged COM1: a 400-byte TX backlog that never drains, yet
Dispose returns in ~0 ms and a full BeginEditorSession/EndEditorSession
cycle closes in 1 ms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RioRuntime raises AxesUpdated with the calibrated AxisOutputs after every
analog reply (independent of the joystick sink, so the gauges work while
editor input routing is suppressed). The editor strip now renders the
values live: Z and the L/R pedals (Rx/Ry) fill bottom-up, Rz deflects
from a center tick, and the X/Y box tracks the stick as a dot over a
crosshair. Fraction/deflection math is pure in Core.Editing.AxisGauges
(clamped, unit-tested); the canvas repaints only the strip and only on
change. Verified by offline form screenshots against injected values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- RIOv4_2.bin: 64K image dumped from the board's AM27C512 (code at
$C000-$FFFF, TMP68HC11).
- disasm_6811.py + RIOv4_2.disasm.asm: vector-rooted 68HC11 disassembly;
SCI ISR at $D630 traced to the $2521 reply-in-progress latch leak that
wedges the analog reply path under button-mash stress.
- make_patch.py + RIOv4_2_patched.bin: two in-place edits (abort-path stub
at $DFF0, unconditional latch clear at $DA21) statically verified by
re-disassembly diff. Dynamic proof awaits a burned W27C512.
- Analysis + burn/validation plan in RIOv4_2-ANALYSIS.md and README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Edit cell boxes" mode: the selected cell grows resize handles on the
preview -- drag to move/resize with live outline tracking and a re-render
on drop, or type exact X/Y/W/H in the new geometry fields. Plain clicks
still select and cycle stacked cells (click-vs-drag resolved by a movement
threshold on mouse-up). The move/resize math is pure and unit-tested
(OverlayRegionEdit: corner/edge/move handle hit-testing with tolerance,
min-size clamped resizing that pins the opposite edge). "Save template"
persists the shared cell geometry back to the regions.json; "Reload
template" re-reads it to discard unsaved box edits. Cursor feedback per
handle; template save/reload disabled when no template path was supplied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The interactive replacement for the legacy Sheet -> GIMP/Script-Fu pipeline:
WallpaperMakerForm (tray -> "Wallpaper maker") renders the profile wallpaper
live via SkiaOverlayRenderer, outlines all 119 template cells, and lets any
cell -- including the heading/banner cells the button editor cannot reach --
be clicked and retitled with debounced re-render. Clicks on stacked cells
cycle through the regions at that pixel (OverlayHitTester, unit-tested);
stacking is legitimate chroma-split display encoding. Imports a legacy .data
sheet row (with a game picker for multi-row sheets), exports the PNG, and can
apply the desktop wallpaper immediately to the same per-profile path the
runtime uses. Prompts for and remembers AppConfig.OverlayTemplatePath on
first use. PLAN.md: record this + the completed Phase 6 deploy package.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The TeslaConsole launcher starts and stops RIOJoy, so the app no longer
registers any auto-start entry:
- install-rio.ps1: drop the HKLM ...\Run registration and the post-install
launch (and the -NoLaunch param).
- Tray: remove the "Start with Windows" menu item and delete AutoStartManager
(HKCU ...\Run writer).
- AppConfig: drop the now-inert AutoStart field (+ test).
- Docs (PLAN.md, README-DEPLOY.txt) updated to reflect launcher-managed start/stop.
uninstall-rio.ps1 still clears any leftover Run entry from older auto-starting
builds. Solution builds; 241 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SkiaSharp drags in win-x64/x86/arm64 native libs plus a macOS .dylib
regardless of RID. The net48 loader finds the native in either the app
root or an arch subfolder (both verified), so keep only the win-x64
libSkiaSharp.dll at the app root and drop the x64/x86/arm64 subdirs and
the .dylib. Package drops from ~25 MB to ~8 MB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documentation now describes the .NET Framework 4.8 stack (was .NET 8):
- README build prerequisites + framework-dependent/no-runtime-install note,
test count 136 -> 241.
- PLAN.md architecture diagram, stack decision (with PolySharp/shims note),
suite total 136 -> 241.
- deploy/README-DEPLOY.txt: app is net48 framework-dependent (4.8 is in-box on
Win10/11), and build prerequisites.
Also migrate the three tools (RioJoySmokeTest, RioSerialMonitor, XcfRegionExtract)
to net48 so they keep building against the now-net48 RioJoy.Core (a net8 project
cannot reference a net48 one). Added PolySharp + System.Memory/System.Text.Json
shims and replaced net-core-only APIs (string.Contains/IndexOf with comparison,
Array.Fill, generic Enum.IsDefined, int.TryParse(span)). All three build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retargets RioJoy.Core/Overlay/Tray + tests from net8.0-windows to net48 so
the app can be tested as a framework-dependent build (relies on the in-box
.NET Framework 4.8 on Windows 10/11). Builds clean; all 241 tests pass.
Polyfills (no behavior change):
- PolySharp source generator for init/records/Index/Range/required members.
- System.Memory, System.Text.Json, Microsoft.Bcl.HashCode,
System.Threading.Channels (tests) NuGet packages.
- Compat/Net48Polyfills.cs: GetValueOrDefault, KeyValuePair.Deconstruct,
Math.Clamp; tests/TestPolyfills.cs: Task.WaitAsync.
Source adjustments for APIs absent on net48:
- ArgumentNullException/ArgumentException.ThrowIf* inlined to manual guards.
- Convert.ToHexString, Encoding.Latin1, Environment.ProcessPath,
ApplicationConfiguration.Initialize, Enum.GetNames<T>/GetValues<T>,
string.StartsWith(char), string.Split(char, opts), TextBox.PlaceholderText,
PeriodicTimer, Memory-based Stream Read/WriteAsync, array range-slicing.
- Dropped [SupportedOSPlatform] hints (net48 is single-platform).
deploy/build-package.ps1: publish framework-dependent (no self-contained).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the package payload folder to RIOJoy (was riojoy) so it extracts
to C:\games\RIOJoy, and add a pre-uninstall path the console can run
before deleting the folder.
- pre-uninstall.bat (inside RIOJoy\): self-elevating entry point that
runs uninstall-rio.ps1.
- uninstall-rio.ps1: stops the tray app, removes the HKLM Run entry,
uninstalls ViGEmBus (-KeepDriver to skip), and clears per-user config
for every profile (-KeepConfig to skip). Idempotent and non-fatal.
- build-package.ps1 bundles both; README documents the uninstall flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deploy/ builds a single zip for TeslaConsole: the self-contained tray app and
the signed ViGEmBus installer under a riojoy\ folder, with postinstall.bat at the
root. postinstall elevates and runs riojoy\install-rio.ps1, which installs
ViGEmBus silently (if absent) and registers RIOJoy to start at logon — no cert
trust, test signing, Secure Boot change, or reboot.
Also guard against two instances fighting over the COM port (machine-wide +
per-user autostart both firing): a per-session named mutex makes a second launch
exit immediately. Built zips (dist/) and the bundled installer (deploy/vendor/)
are git-ignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the test-signed RioGamepad driver as the default joystick output with a
ViGEmBus virtual Xbox 360 controller (Nefarius.ViGEm.Client) — properly signed,
so it installs with no test-signing / Secure Boot change / reboot.
The coordinator now selects ViGEm first, then the RioGamepad HID feeder, then a
no-op. The XInput layout caps joystick buttons at 11, so the editor's joystick
picker lists the named Xbox buttons (A, B, X, Y, LB, RB, Back, Start, L3, R3,
Guide); the hat maps to the D-pad and the six axes to the sticks + triggers.
Bind anything beyond 11 to the keyboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Triggers:" field under the profile name that edits the profile's
MatchExecutables (comma-separated, .exe optional). On save, the listed
foreground executables auto-activate the profile in Auto mode — no more
hand-editing config.json.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Profile name field in the editor (rename, with empty/duplicate checks);
"Edit profile" becomes a submenu listing each profile plus "New profile…".
- "Send button output to the PC" toggle: editor sessions route keyboard/mouse
and joystick through gates (GatedInputSink/GatedJoystickSink) that stay closed
until the user opts in, so editing never injects input by default.
- Save profile now confirms ("Saved ✓"/"Save failed") — the write was silent.
- A lit button renders dim at idle and bright only when physically pressed, so
the press stays visible (three states: off / dim-lit / bright-held).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SendInput declared its parameter as Span<INPUT>, which cannot be marshalled
(MarshalDirectiveException) — the first keyboard/mouse output threw on the
serial read thread and killed the link. Pass a blittable INPUT[] instead;
add a regression test.
Also: defend the runtime so a faulty output sink can never tear down the
serial link (output is best-effort), and dim lit lamps on the first reply
from the board rather than in Start() — lamp commands sent in the first ms
after the DTR reset (on port open) are dropped before the board has booted.
Verified end-to-end against the partial RIO on COM1 (keypad types to the PC,
joystick feeds, comms stay up; lit MFD buttons come up dim).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
While a profile is being edited, light every pressed button on the RIO
(RioRuntime.EchoAllLamps, enabled for editor sessions) regardless of mapping,
so a physical press always responds. On the panel, a held button now
brightens the cell in its own colour (red MFD / blue keypad / yellow board)
instead of a cyan outline.
Surface the RIO version/check replies (RioRuntime.VersionReceived/
CheckReceived) and show them grouped in a new "RIO reply" box below the
command buttons when "Request version & status" is clicked. Drop the two
diagnostic readout-toggle buttons (comms bring-up leftovers).
Verified against the partial RIO on COM1 (firmware 4.2; MFD + keypad light
and report; missing-board status shown).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A console diagnostic that opens the RIO COM port, runs the real serial link,
and logs every event (buttons, keypad, axis, version/check, control bytes,
framing). Flashes all lamps once to prove the PC->RIO path and echoes a lamp
on each button press. Verified end-to-end against a partial RIO on COM1
(firmware 4.2; MFD + keypad inputs and the throttle axis all decode).
dotnet run --project tools/RioSerialMonitor -- COM1 30
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Profile editor:
- Cockpit-style encoder gauges centered over the Upper Middle MFD: Z left,
X/Y right, and L/R/Rz arranged as a big "U".
- [JoyStick] section surfaced as a vertical "Axis" toggle column (6 inverts
+ ZR mix), written back to the profile's calibration.
- Tighten the panel layout (close gaps, trim empty border columns); colour the
Secondary/Screen columns yellow; rename the "Joystick / Hat" board to
"Joystick"; add an "Import .ini..." tray entry.
- "RIO commands (live)" button group (all RioCommandCodes) fired at the live RIO.
Serial-port handling:
- Start dormant; only take the COM port for a profiled game (auto-switch) or
while editing, so the native games can open the port without clashing.
- Editor sessions hold the port but route keyboard/mouse/joystick to no-op
sinks (NullInputSink), so button function can be checked without injecting
input; RioRuntime.ButtonActivity drives a live cyan press indicator and
lamp feedback still applies.
- AutoSwitchWatcher.Reset() re-syncs the watcher after an editor session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Strip a surrounding pair of double quotes from the [Plasma] Greeting on
import (the gsheet "INI_V2" output writes Greeting="..."), and add the
master button-layout spreadsheet that documents the RIO.ini format and the
16-bit map-word encoding. Test covers the real sheet output (modifier-stacked
words, keypad VK words, RIO command words) round-tripping verbatim.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworked the profile/mapping editor to mirror the original unfinished Win32
RIO driver's control-panel design (docs/Win32RIO, by FASA / Michel Lowrance)
instead of the flat config-sheet grid. The wallpaper region positions are a
VGA chroma-split display artifact, not the cockpit's logical shape.
- RioJoy.Core.Editing.CockpitPanel: the functional layout — five MFD clusters,
four board columns (Throttle/Secondary/Screen/Joystick-Hat), an encoder-gauge
strip, and the two later-added 4x4 keypads (Internal/External). Places every
address 0x00-0x47 / 0x50-0x6F exactly once (unit-tested).
- Tray PanelView/PanelCanvas render the panel; ProfileEditorForm drives it.
Buttons show label + assigned function (ButtonBinding.Describe -> CTL-N, JOY1,
POV-U, Mse LB, RIO command names; unit-tested). Lamp shade is driven by the
IsLit flag (not by whether a function is assigned); keypads are neutral blue
with no Lit checkbox. Added an Unassign button next to Apply.
- Removed the superseded sheet-grid UI (SheetView/SheetCanvas); SheetLayout +
config-sheet.csv stay as reference data.
docs/Win32RIO: the original driver (tasgame.sys, a 32-bit kernel HID minidriver
that opened the serial port, set baud/8N1/DTR, and spoke the *identical* RIO
protocol -- AnalogRequest/Reply, Button Pressed/Released, Check/Lamp/Reset/
Version -- validating our protocol port), oemsetup.inf, RemoteDriver.doc, and the
extracted control-panel / game-controllers mockups.
~236 xUnit tests green. PLAN.md updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Overlay generator (pure, tested) — RioJoy.Core/Overlay:
- FontFitter ports the legacy calc-fontsize auto-fit search (validated against a
brute-force oracle); OverlayLayoutEngine ports create-data-layer's fit + h/v
justification and adds per-region 90 deg CCW rotation; OverlayTemplate/Region +
OverlayTemplateStore hold cell geometry/colour/rotation (regions.json).
- GoobieDataImporter parses the legacy .data label sheet into label rows.
- src/RioJoy.Overlay: SkiaSharp rasterizer (SkiaTextMeasurer shared with the engine
so measured layout == drawn output; SkiaOverlayRenderer -> PNG;
ProfileWallpaperGenerator). Verified end-to-end on the real cockpit art
(regions.json + riojoy.png + TEST.data) by OverlayRenderIntegrationTests.
- RioJoy.Tray/WallpaperApplier applies via SystemParametersInfo; RioCoordinator
generates+applies on profile activation (opt-in via AppConfig.OverlayTemplatePath).
Region geometry — tools/XcfRegionExtract parses riojoy.xcf (a GIMP-format binary
reader, no GIMP needed) into the 119-cell regions.json: per-layer offsets/size/
font/colour, with 90 deg CCW rotation on a-00 + b-10..b-1F. Base image riojoy.png.
NOTE: the wallpaper positions are a VGA chroma-split display artifact (6 displays),
not the cockpit's logical layout.
Mapping editor (first cut) — RioJoy.Tray/Editor/ProfileEditorForm renders the
config sheet's logical grid (SheetLayout parses the sheet CSV export). The iRIO
word is edited via ButtonBinding <-> RioMapEntry.Create (no hex bit-twiddling), with
a context-sensitive picker: keyboard keys by name (KeyCatalog VK names), joystick
Button N, hat direction, mouse/RIO-command enum names; modifiers only for keyboard.
Opened from the tray ("Edit profile..."). The sheet-grid layout is a starting point
that needs rework from a better-formatted source.
~220 xUnit tests green. Docs (PLAN.md, README) updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RioGamepad driver now installs and enumerates, so the user-mode
feeder path is verifiable. Add tools/RioJoySmokeTest, a standalone
on-cabinet utility that drives the real HidFeederJoystickSink (open the
device, submit reports via IOCTL_RIO_SUBMIT_REPORT) and reads the gamepad
back through winmm joyGetPosEx, asserting axes (min/mid/max), buttons,
and the POV hat all surface to the OS. Verified: all checks pass against
the installed driver.
Update docs to match reality (PLAN.md predated the HidFeederJoystickSink
commit): Phase 1 is now test-signed/installed/verified with the VHF
LowerFilters requirement noted; the stale "NullJoystickSink placeholder"
remainders in Phases 3 and 5 are corrected to reflect the wired,
verified feeder. driver/README.md notes the end-to-end verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The driver installed but the device failed with Code 31
(CM_PROB_FAILED_ADD): VhfCreate returned STATUS_INVALID_DEVICE_REQUEST
(0xC0000010) because vhf.sys was never attached beneath the FDO. Add the
required LowerFilters=vhf AddReg to the INF so VHF loads as a lower
filter; the device now starts clean and enumerates in joy.cpl.
A VHF virtual HID device cannot supply a HID product string (VHF_CONFIG
has no string field and VHF owns IOCTL_HID_GET_STRING), so register the
DirectInput OEMName (VID_1209&PID_5249 -> RIOJoy Virtual Gamepad) in
install.ps1 -CreateDevice and remove it in uninstall.ps1, giving the
controller a proper name instead of the generic VHF default.
Also fix a non-ASCII em-dash in sign.ps1 that broke parsing under
Windows PowerShell 5.1 (UTF-8 without BOM), and document the Code 52 /
Code 31 / friendly-name troubleshooting in driver/README.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the virtual gamepad deployable on owned cabinets and wire the real
user-mode feeder:
- driver/sign.ps1 (non-admin): create a self-signed code-signing cert, build the
catalog with inf2cat, and SHA-256-sign RioGamepad.sys + .cat (embed-sign the sys
before cataloguing so the .cat matches). Exports RIOJoyTest.cer. Verified
end-to-end against the EWDK (signability + catalog clean; both files signed).
- driver/install.ps1 (admin, two-phase): trust the cert (LocalMachine Root +
TrustedPublisher), stage the package (pnputil /add-driver), enable test signing;
after reboot, -CreateDevice runs devgen to create root\RioGamepad so PnP installs
it. uninstall.ps1 reverses it.
- RioJoy.Core.Output.HidFeederJoystickSink: opens the driver by
GUID_DEVINTERFACE_RIOGAMEPAD (SetupAPI), maintains a RioHidReport, and submits it
via DeviceIoControl(IOCTL_RIO_SUBMIT_REPORT) on each axis/button/hat change.
RioCoordinator now uses it when the driver is present and falls back to the no-op
sink otherwise (status shows "[no joystick driver]").
- gitignore the signing outputs (driver/package/, *.cat); driver/README.md gets the
full build → sign → install → joy.cpl workflow.
Remaining = the actual elevated install + reboot + joy.cpl verification on the
cabinet (admin steps), and on-hardware confirmation of the feeder.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author the custom virtual HID gamepad that replaces vJoy, and pin its wire
format on both sides. Builds clean to RioGamepad.sys against the EWDK
(KMDF 1.15 + VHF, x64, warnings-as-errors).
driver/RioGamepad/:
- ReportDescriptor.h: 6x16-bit axes (X,Y,Z,Rx,Ry,Rz), one 4-direction hat with
null state, and 96 buttons — the legacy vJoy layout. 25-byte input report.
- Device.c/Driver.c: KMDF root-enumerated device that creates the VHF virtual HID
device (VhfCreate in DeviceAdd, VhfStart in D0Entry, VhfDelete on cleanup) and
exposes a device interface + IOCTL_RIO_SUBMIT_REPORT that forwards the caller's
report bytes to VhfReadReportSubmit. Thin relay: no report logic in the kernel.
- Public.h: device-interface GUID, IOCTL, and the report byte layout shared with
the C# client. RioGamepad.inf + build.cmd (EWDK build, catalog/sign disabled).
src/RioJoy.Core/Hid/RioHidReport.cs: packs AxisOutputs + hat + 96 buttons into
the exact 25-byte report (LE axes, hat nibble with 0x0F=centered, button bitmap).
13 new xUnit tests (136 total).
Remaining (deploy-side): test-sign + pnputil install + verify in joy.cpl, and
wire the real DeviceIoControl feeder sink (replacing NullJoystickSink). The
EWDK's in-build catalog task (DrvCat) can't load Microsoft.Kits.Logger on this
image, so the .cat is produced with inf2cat/signtool at install time instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the Core pieces into a runnable tray app with per-game profiles and the
three-state serial-yield auto-switch:
- Profiles/: RioProfile + AppConfig model; ConfigStore (System.Text.Json,
round-tripped); RioIniImporter ports the legacy RIO.ini (button table, invert
flags, plasma greeting); AutoSwitchResolver + AutoSwitchWatcher resolve the
foreground executable into Yield (native game) / Activate (profile) / Idle, with
native always winning and change-only notifications. IForegroundProcessProvider
abstracts the OS.
- RioRuntime assembles a profile's live pipeline: serial ButtonPressed/Released +
KeyPressed/Released → InputRouter (via RioAddress); AnalogReply → AxisCalibrator
→ the six joystick axes; RIO commands → calibration resets + version/check
requests + lamp re-init. SerialLampSink sends lamp feedback over the link;
NullJoystickSink is a placeholder until the Phase 1 HID feeder exists.
- RioJoy.Tray: NotifyIcon menu mirroring the legacy console menu (axis resets,
version/status, raw-axes & poll-rate 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 (Win32 foreground PID→exe) and AutoStartManager (HKCU
Run key).
- tests: 18 new xUnit tests (123 total) for config round-trip, ini import,
the three-state resolver + watcher, and RioRuntime end-to-end over the fake
transport (button→joystick, keypad-offset→keyboard, analog→six axes).
The joystick output stays a no-op until the Phase 1 driver; on-cabinet
verification of the acquire/release lifecycle remains.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the analog calibration math and the plasma/VFD command set:
- Calibration/: AxisCalibrator ports UpdateThrottle/UpdatePadal/UpdateJoystick
(riovjoy2.cpp#L1504+) — throttle deadzone + ratchet field, pedal deadzones,
joystick X/Y auto-ranging from observed min/max, rudder mixing (enableZR), and
the per-axis invert flags. Stateful (start positions, last outputs, observed
extremes) like the legacy globals, with the RIOcmd axis resets. Final outputs
clamp to the documented 0..32766 range (also guards a legacy compounding quirk).
AxisOutputs carries the six values; IJoystickSink gains SetAxis(JoyAxis,value)
so calibrated axes reach the HID feeder.
- Plasma/: PlasmaCommands builds the CPlasma ESC sequences (clear/cursor/font/
attr/box draw+fill/text) + GetFontSize + the PlasmaPosText auto-fit/centering;
PlasmaDisplay writes them over the secondary COM transport.
- tests: 21 new xUnit tests (105 total) for throttle proportional/saturation/
invert, pedal ZR mix vs. direct, joystick centering/direction/invert/reset, the
output clamp, and plasma byte sequences/font sizes/auto-fit positioning.
Hardware verification of axis feel + plasma output remains; the game-specific
PlasmaScoreDraw layout is deferred to profile content (Phase 5/7).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the iRIO decode and Press_V2/Release_V2 routing into testable C#:
- Mapping/: RioMapEntry decodes the 16-bit map word (lamp/mouse/hat/joy/extended/
alt/ctrl/shift flags + value) and resolves the routing Kind by the legacy
precedence (joy+hat+mouse => RIO command; none => keyboard; else joy>hat>mouse).
RioAddress translates button/keypad events to table addresses (+0x50/+0x60
keypad offsets); RioInputMap is the 112-entry per-profile table replacing the
hard-coded iRIO[].
- InputRouter ports Press_V2/Release_V2: modifier press/release ordering,
scancode keys, joystick buttons, POV hat, mouse move/click (deltas corrected
per PROTOCOL.md), RIO-command dispatch, and lamp feedback (bright on press, dim
on release; RIO commands carry none). InitializeLamps() dims all lamp entries.
- Output is split behind sink interfaces (IInputSink/IJoystickSink/ILampSink/
IRioCommandSink) so routing is pure + unit-tested; Output/SendInputSink is the
real SendInput keyboard/mouse adapter (scancode injection).
- tests: 30 new xUnit tests (84 total) for entry decode, address translation,
and router routing/precedence/lamp/modifier ordering via a recording sink.
The joystick sink's real adapter (HID feeder -> RioGamepad via DeviceIoControl)
is blocked on the Phase 1 driver; routing already targets IJoystickSink.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the RIO wire protocol from legacy/riovjoy2.cpp into testable C#:
- Protocol/: command + length table, 7-bit checksum, packet builder, and a
streaming receive-side framing state machine (PacketParser) that mirrors the
legacy ReadCommBlock framing/resync (high-bit-mid-packet abort). Typed RIO->PC
decodes: AnalogReport (14-bit sign-extend), VersionInfo, CheckStatus; lamp-state
composition.
- Serial/: RioSerialLink drives an async receive loop with ACK/NAK reply policy
(legacy force-accept vs. opt-in VerifyInboundChecksum), the analog poll timer,
and the >5s reset-recovery watchdog. IRioTransport abstracts the COM port; the
SerialPort-backed transport does 9600 8N1 + DTR reset pulse, and acquire/release
is just create/dispose (foundation for native-game serial yield).
- tests/RioJoy.Core.Tests: 54 xUnit tests covering checksum, framing/resync,
builder round-trips, analog sign-extension + sentinel rejection, lamp combos,
and the read loop driven against an in-memory fake transport.
Hardware verification (version/check/analog against a cabinet) remains; it can't
be done off-device.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Modernization of the legacy vJoy-based RIO cockpit interface for Win10/11,
removing the vJoy dependency in favor of a custom VHF/UMDF HID driver,
rewritten in C#/.NET 8 as a background tray app with per-game profiles.
- Reorganize: legacy C++ -> legacy/, cockpit art -> docs/reference/
- RioJoy.sln: src/RioJoy.Core (lib) + src/RioJoy.Tray (tray app), net8.0-windows x64
- driver/ placeholder for the RioGamepad WDK driver
- docs/PLAN.md (7-phase plan; profiles + serial-yield model)
- docs/PROTOCOL.md (RIO wire format + iRIO input-map reference)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>