Phase 8C: RioJoy.Tray multi-targets net48 (x64) + net40 (x86, Windows XP)

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>
This commit is contained in:
Cyd
2026-07-11 20:45:34 -05:00
co-authored by Claude Fable 5
parent 1ff0b16015
commit 63af7bde01
5 changed files with 71 additions and 6 deletions
@@ -261,9 +261,15 @@ public sealed class ProfileEditorForm : Form
_valueCombo.Items.Add(new ValueItem(k.Name, k.Value));
break;
case RioRouteKind.Joystick:
#if NET40
// XP: RioGamepadXP exposes the full 96-button HID layout.
for (int btn = 1; btn <= RioJoy.Core.Hid.RioHidReport.ButtonCount; btn++)
_valueCombo.Items.Add(new ValueItem($"Button {btn}", (byte)btn));
#else
// The signed ViGEmBus Xbox 360 pad exposes 11 buttons; show their names.
for (int btn = 1; btn <= ViGEmJoystickSink.MappableButtonCount; btn++)
_valueCombo.Items.Add(new ValueItem($"Button {btn} ({ViGEmJoystickSink.ButtonNames[btn - 1]})", (byte)btn));
#endif
break;
case RioRouteKind.Hat:
foreach (RioHat h in new[] { RioHat.Up, RioHat.Right, RioHat.Down, RioHat.Left })
+20 -1
View File
@@ -5,7 +5,9 @@ using RioJoy.Core.Output;
using RioJoy.Core.Overlay;
using RioJoy.Core.Profiles;
using RioJoy.Core.Serial;
using RioJoy.Overlay;
#if !NET40
using RioJoy.Overlay; // SkiaSharp wallpaper generation — modern flavor only
#endif
namespace RioJoy.Tray;
@@ -149,12 +151,14 @@ public sealed class RioCoordinator : IDisposable
// feeder, then a no-op when neither driver is present.
private IJoystickSink CreateJoystickSink(out string note)
{
#if !NET40 // ViGEmBus is Win10+; on XP the HID feeder drives RioGamepadXP.sys
if (ViGEmJoystickSink.TryCreate(out ViGEmJoystickSink? vigem))
{
_joystick = vigem;
note = string.Empty;
return vigem!;
}
#endif
if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
{
@@ -241,6 +245,20 @@ public sealed class RioCoordinator : IDisposable
/// </summary>
private void ApplyWallpaper(RioProfile profile)
{
#if NET40
// XP flavor: no SkiaSharp, so no generation — apply the profile's
// pre-rendered wallpaper (authored on a modern machine) if it exists.
if (string.IsNullOrWhiteSpace(profile.WallpaperPath) || !File.Exists(profile.WallpaperPath))
return;
try
{
WallpaperApplier.Apply(profile.WallpaperPath!);
}
catch (Exception ex)
{
SetStatus($"{Status} [wallpaper: {ex.Message}]");
}
#else
string? templatePath = _config().OverlayTemplatePath;
if (string.IsNullOrWhiteSpace(templatePath) || !File.Exists(templatePath))
return;
@@ -263,6 +281,7 @@ public sealed class RioCoordinator : IDisposable
{
SetStatus($"{Status} [wallpaper: {ex.Message}]");
}
#endif
}
private static string SafeFileName(string name)
+25 -5
View File
@@ -2,9 +2,12 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net48</TargetFramework>
<!-- net48 = Windows 10/11 (x64, full app incl. wallpaper maker/SkiaSharp).
net40 = Windows XP SP3 (x86): full runtime + mapping editor; wallpaper
GENERATION is excluded (SkiaSharp has no XP support) — XP applies
pre-rendered wallpapers only. See docs/PLAN.md §Phase 8. -->
<TargetFrameworks>net48;net40</TargetFrameworks>
<Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
@@ -12,12 +15,29 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RioJoy.Core\RioJoy.Core.csproj" />
<ProjectReference Include="..\RioJoy.Overlay\RioJoy.Overlay.csproj" />
<PropertyGroup Condition="'$(TargetFramework)' == 'net48'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net40'">
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
<!-- net40 has no System.Net.Http assembly for the implicit global using. -->
<Using Remove="System.Net.Http" />
<!-- Wallpaper maker needs the SkiaSharp renderer (RioJoy.Overlay) — modern only. -->
<Compile Remove="Editor\WallpaperMakerForm.cs" />
<Compile Remove="Editor\WallpaperCanvas.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RioJoy.Core\RioJoy.Core.csproj" />
<ProjectReference Include="..\RioJoy.Overlay\RioJoy.Overlay.csproj" Condition="'$(TargetFramework)' == 'net48'" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -74,10 +74,12 @@ internal sealed class TrayApplicationContext : ApplicationContext
RebuildEditMenu(editMenu);
menu.Items.Add(editMenu);
#if !NET40 // wallpaper maker needs SkiaSharp — modern flavor only (XP applies pre-rendered)
var wallpaperMenu = new ToolStripMenuItem("Wallpaper maker");
wallpaperMenu.DropDownOpening += (_, _) => RebuildWallpaperMenu(wallpaperMenu);
RebuildWallpaperMenu(wallpaperMenu);
menu.Items.Add(wallpaperMenu);
#endif
menu.Items.Add("Import .ini…", null, (_, _) => ImportIniProfiles());
@@ -155,6 +157,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
editMenu.DropDownItems.Add(new ToolStripMenuItem("New profile…", null, (_, _) => OpenEditor(NewProfile())));
}
#if !NET40
// Rebuild the "Wallpaper maker" submenu: one entry per profile.
private void RebuildWallpaperMenu(ToolStripMenuItem wallpaperMenu)
{
@@ -205,6 +208,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
"RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
#endif
// Create, register and persist a fresh empty profile with a unique name.
private RioProfile NewProfile()
+16
View File
@@ -30,6 +30,22 @@ public static class WallpaperApplier
if (!File.Exists(full))
throw new FileNotFoundException("Wallpaper image not found.", full);
#if NET40
// Windows XP's SPI_SETDESKWALLPAPER accepts only BMP. Wallpapers are
// authored as PNG on a modern machine, so convert beside the source on
// apply (idempotent: reuses an up-to-date .bmp).
if (!full.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase))
{
string bmp = Path.ChangeExtension(full, ".bmp");
if (!File.Exists(bmp) || File.GetLastWriteTimeUtc(bmp) < File.GetLastWriteTimeUtc(full))
{
using var image = System.Drawing.Image.FromFile(full);
image.Save(bmp, System.Drawing.Imaging.ImageFormat.Bmp);
}
full = bmp;
}
#endif
return SystemParametersInfo(
SPI_SETDESKWALLPAPER, 0, full, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
}