The right-hand panel gains a "RIO port" row under Triggers: an editable
combo offering the app default (shown with its value), the machine's COM
ports, and pipe:vrio, with free text for anything else. Save stores the
endpoint in RioProfile.RioComPort (blank or the default entry = null =
follow DefaultRioComPort).
Because the editor session holds the endpoint it opened with, a saved
port change now re-arms the session live: the tray host unhooks the old
runtime's editor wiring, re-activates on the new endpoint, re-hooks, and
restores the output-gate state - so the live RIO commands and button
echo follow the new port/pipe without closing the editor.
Verified with the offline DrawToBitmap harness (layout, both default and
pipe:vrio states) plus a scripted save round-trip (default->null,
pipe:vrio->stored, blank->null, reopen shows stored COM7).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NamedPipeTransport connects as a client to vRIO's \\.\pipe\vrio (the
DOSBox-X fork's role) and speaks the shared typed-frame contract
(PipeFraming: 0x00 data / 0x01 modem lines, null-modem crossed). The COM
path's DTR reset pulse is replayed in-band on connect. Peer disconnects
and framing violations surface as the 0-byte transport-closed read the
link already understands.
RioTransportFactory routes endpoint strings — pipe:name (vRIO's own
picker syntax) to the pipe transport, everything else to
SerialPortTransport — and is wired into RioCoordinator and all three
RioSerialMonitor modes, so profiles (RioComPort/DefaultRioComPort) and
the bench tools take pipe endpoints anywhere a COM name went.
Gotcha baked into the design: named pipes here have 0-byte buffers, so a
write blocks until the peer reads it, and vRIO also writes its lines
frame before reading — the on-connect pulse frames are therefore queued
as overlapped writes (pipe writes drain in issue order, preserving the
edge positions) instead of blocking the constructor into a mutual
write-first deadlock.
Verified end-to-end against the real VRioDevice + VRioPipeService over
\\.\pipe\vrio: version 4.2 + check replies, 137 analog polls, lamp
commands ACKed, zero framing errors. 322 tests green, both flavors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RioJoy.Tray --import-profile <profile.json> merges a single-profile
document (a repo's profiles/*.json) into %APPDATA%\RIOJoy\config.json
via ConfigStore.ImportProfile: same-name profiles are replaced in place
(FindProfile's case-insensitive convention), everything else appended,
all other config content preserved. Refuses (exit 3) while a tray
instance is running - a live tray rewrites the config from memory and
would silently discard the import. Documents that a Name must be stated
in the file itself: RioProfile defaults Name to 'Unnamed', so the import
checks the raw JSON, not the deserialized object. This is the profile
install story for bench machines and pods (previously: hand-paste into
the Profiles array).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RioProfile gains a nullable AxisRouting section mapping each calibrated
axis (X/Y/Z/Rx/Ry/Rz) to a pad target (thumbs, triggers, or None) with a
Centered or UnipolarPositive conversion; the default reproduces the old
hardcoded routing exactly, so existing profiles are untouched. Routing
resolution lives in a pure, ViGEm-free AxisRouter for testability; the
sink neutralizes the pad on routing change so stale trigger state cannot
leak across profile switches.
Motivation: Descent reads the pad via SDL GameController, where the
triggers are its stock fire axis-buttons - the old fixed routing put
throttle on LeftTrigger (fires) and detent would have read as full
reverse. descent-d1x.json now routes Z->RightThumbY (UnipolarPositive,
detent = center) and Rz->RightThumbX, triggers untargeted; guarded by
tests that parse the shipped JSON through the real deserializer and
byte-compare the dxx-rebirth reference copy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Environment drift since ~7/26 broke a pristine checkout: (1) the SDK's
AutoGenerateBindingRedirects made RAR drop RioJoy.Core.dll for the net40
Tray exe (Bcl facade 1.5.11.0 vs 2.6.8.0 mismatch) - disabled for net40
with a hand-authored app.net40.config carrying the 2.6.8.0 redirects,
which XP's CLR 4.0 does not unify on its own; (2) 25 serial-link tests
failed on System.Runtime.CompilerServices.Unsafe load - Channels 8.0's
net462 binary references Unsafe 6.0.0.0 without declaring the dependency;
pinned the package and added the redirect app.config the earlier good
builds had auto-generated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wallpaper maker/runtime overrides the desktop with the cockpit
wallpaper on profile activation but never put the user's own back.
Now RioCoordinator captures the current wallpaper (SPI_GETDESKWALLPAPER,
via new WallpaperApplier.GetCurrent) the first time it overrides it, and
restores it (WallpaperApplier.Restore) on every GoDormant and on Dispose
— so going idle, a native game taking the port, or app exit returns the
desktop to what the user had. Capture is once-per-override so switching
between cockpit profiles keeps the real previous wallpaper; only engages
when OverlayTemplatePath is set. Builds clean on net48 + net40.
Known gap (documented in PLAN.md): a hard crash between apply and restore
leaves the cockpit wallpaper, since SPIF_UPDATEINIFILE persists it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Operator-verified zero lamp misses/hangs through the slow-chord test
(62500's killer) and 392/min mash. Final run: NAK=0, 25 timeout resends
silently healing inbound response loss, counters flat, zero wedges.
--poll 20 delivered ~31Hz analog (1.7x legacy); the shortfall vs 50Hz
is additive host pacing (delay after awaited exchange), not board
saturation - noted as a future host tweak.
AppConfig gains RioBaudRate (default 9600) + AnalogPollMs (default 55),
plumbed through RioCoordinator; existing configs unchanged. Production
matrix recorded in ANALYSIS.md: 9600 patched chip for native-game
cabinets, 31250v2 + poll 20-25 + FTDI for RIOJoy cockpits.
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>
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>
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>
"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>
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>
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>
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>
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>
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>
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>
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>
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>