Docs: reflect net48; convert tools projects to net48

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>
This commit is contained in:
Cyd
2026-06-30 13:11:25 -05:00
co-authored by Claude Opus 4.8
parent fe87c79f55
commit 270abfc5ad
9 changed files with 51 additions and 16 deletions
+6 -3
View File
@@ -27,8 +27,11 @@ Red Planet — talk to the RIO directly and do not use this app.)
## Building
Requires the **.NET 8 SDK** and Windows. The driver builds separately with the
**WDK** (see [`driver/README.md`](driver/README.md)).
Requires the **.NET SDK** (8.0 or newer) plus the **.NET Framework 4.8
targeting/developer pack**, on Windows. The apps target **.NET Framework 4.8**,
which is in-box on every Windows 10/11 machine — so deployed builds are
framework-dependent and need **no runtime install** on the target. The driver
builds separately with the **WDK** (see [`driver/README.md`](driver/README.md)).
```sh
dotnet build RioJoy.sln -c Release
@@ -37,7 +40,7 @@ dotnet test RioJoy.sln
## Status
Phases 15 are implemented and tested (136 unit tests). The `RioGamepad` virtual
Phases 15 are implemented and tested (241 unit tests). The `RioGamepad` virtual
HID driver is built (KMDF + VHF), **test-signed, installed, and verified**: it
enumerates in `joy.cpl`, and the C# HID feeder (`DeviceIoControl`
`RioGamepad.sys`) drives its axes, buttons, and hat end-to-end (see
+5 -3
View File
@@ -11,8 +11,9 @@ Contents
postinstall.bat Entry point (at the root). Elevates, then runs
RIOJoy\install-rio.ps1.
RIOJoy\ The payload folder:
app\ The RIOJoy tray application (self-contained .NET 8 — no runtime
install required on the target machine).
app\ The RIOJoy tray application (.NET Framework 4.8, framework-
dependent). .NET Framework 4.8 is in-box on Windows 10/11, so
no runtime install is required on the target machine.
vendor\ The signed ViGEmBus installer (Nefarius, WHQL/attestation-signed).
install-rio.ps1 The actual install steps (idempotent).
pre-uninstall.bat Cleanup entry point. Elevates, then runs uninstall-rio.ps1.
@@ -57,7 +58,8 @@ After install
Building this package (on a dev machine)
-----------------------------------------
Requires the .NET 8 SDK and the signed ViGEmBus installer.
Requires the .NET SDK (8.0+) with the .NET Framework 4.8 targeting/developer
pack, and the signed ViGEmBus installer.
powershell -ExecutionPolicy Bypass -File deploy\build-package.ps1 `
-VigemInstaller <path-to-ViGEmBus_x.y.z_x64_x86_arm64.exe>
+7 -3
View File
@@ -25,7 +25,7 @@ cockpit's outputs on their behalf.
## Target architecture
```
┌─────────────────────── C# / .NET 8 tray app (x64) ───────────────────────┐
┌────────────────── 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 ↓
@@ -46,7 +46,11 @@ cockpit's outputs on their behalf.
- **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.
- **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), auto-start with Windows.
- **Targets:** Windows 10/11 **x64 only**. The legacy x86 / WinXP targets are
dropped.
@@ -183,7 +187,7 @@ Implemented in `src/RioJoy.Core/Calibration` + `Plasma` (105 xUnit tests total):
### 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):
(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
+1 -1
View File
@@ -105,7 +105,7 @@ int FindJoyId()
if (firstPresent < 0) { firstPresent = id; lastName = name; }
// RioGamepad identity (Public.h: RIO_VENDOR_ID 0x1209 / RIO_PRODUCT_ID 0x5249).
if (caps.wMid == 0x1209 && caps.wPid == 0x5249) { lastName = name; return id; }
if (name.Contains("RIOJoy", StringComparison.OrdinalIgnoreCase)) { lastName = name; return id; }
if (name.IndexOf("RIOJoy", StringComparison.OrdinalIgnoreCase) >= 0) { lastName = name; return id; }
}
return firstPresent;
}
+8 -1
View File
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net48</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget>
<AssemblyName>RioJoySmokeTest</AssemblyName>
@@ -12,4 +13,10 @@
<ItemGroup>
<ProjectReference Include="..\..\src\RioJoy.Core\RioJoy.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
+2 -2
View File
@@ -52,7 +52,7 @@ AnalogReport lastLogged = default;
bool haveAnalog = false;
TimeSpan lastAnalogLog = TimeSpan.Zero;
var min = new short[5]; var max = new short[5];
Array.Fill(min, short.MaxValue); Array.Fill(max, short.MinValue);
for (int i = 0; i < min.Length; i++) { min[i] = short.MaxValue; max[i] = short.MinValue; }
void Track(AnalogReport r)
{
@@ -113,7 +113,7 @@ link.AnalogReceived += r =>
link.VersionReceived += v => { version = v; Log($"VERSION reply: firmware {v}"); };
link.CheckReceived += c => { checks.Add(c); Log($"CHECK reply: {c}"); };
link.ControlReceived += b => { controls++; Log($"control byte 0x{b:X2} ({(Enum.IsDefined((RioControl)b) ? (RioControl)b : (object)"?")})"); };
link.ControlReceived += b => { controls++; Log($"control byte 0x{b:X2} ({(Enum.IsDefined(typeof(RioControl), (RioControl)b) ? (RioControl)b : (object)"?")})"); };
link.FramingError += () => { framing++; Log("FRAMING ERROR (resync)"); };
using var cts = new CancellationTokenSource();
+12 -1
View File
@@ -2,9 +2,12 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net48</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget>
<AssemblyName>RioSerialMonitor</AssemblyName>
</PropertyGroup>
@@ -12,4 +15,12 @@
<ProjectReference Include="..\..\src\RioJoy.Core\RioJoy.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
+1 -1
View File
@@ -30,7 +30,7 @@ ulong U64() { ulong v = 0; for (int i = 0; i < 8; i++) v = (v << 8) | d[pos++];
string magic = Encoding.ASCII.GetString(d, 0, 9); // "gimp xcf "
string ver = Encoding.ASCII.GetString(d, 9, 4); // "file" (=v0) or "vNNN"
if (magic != "gimp xcf ") { Console.Error.WriteLine($"Not an XCF file: '{magic}'"); return 1; }
bool ptr64 = ver[0] == 'v' && int.TryParse(ver.AsSpan(1, 3), out int vn) && vn >= 11;
bool ptr64 = ver[0] == 'v' && int.TryParse(ver.Substring(1, 3), out int vn) && vn >= 11;
long Ptr() => ptr64 ? (long)U64() : U32();
pos = 14;
@@ -1,10 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net48</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<AssemblyName>XcfRegionExtract</AssemblyName>
<!-- Dev utility; not part of RioJoy.sln. -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>