-
Notifications
You must be signed in to change notification settings - Fork 6
Build and Publish
Build, publish, and release reference for PadForge.
PadForge.sln
├── PadForge.Engine/ Class library (net10.0-windows)
│ ├── Common/ SDL3 P/Invoke, input types, device wrappers
│ ├── Data/ PadSetting, UserDevice, UserSetting
│ └── Properties/ AssemblyInfo.cs
│
├── PadForge.App/ WPF application (net10.0-windows10.0.26100.0)
│ ├── Common/ SettingsManager, DriverInstaller, InputManager pipeline
│ ├── Views/ XAML pages and code-behind
│ ├── ViewModels/ MVVM ViewModels
│ ├── Services/ InputService, SettingsService, DeviceService, etc.
│ ├── WebAssets/ HTML/CSS/JS for browser virtual controller
│ ├── Models3D/ 3D model classes (Xbox360 / XboxOne / DS4 / DualSense)
│ ├── Models2D/ 2D overlay layout classes (5 layouts)
│ ├── 2DModels/ PNG overlay images (DS4/, DualSense/, XBOX360/, XBOXONE/, XBOXSERIES/)
│ ├── 3DModels/ OBJ meshes (DS4/, DualSense/, XBOX360/, XBOXONE/)
│ ├── Converter/ WPF value converters
│ ├── Controls/ Custom controls (RangeSlider)
│ ├── Resources/ Icons, SDL3 DLL, embedded driver installers, localization
│ └── Properties/ AssemblyInfo.cs
│
├── nuget-local/ Local NuGet source (MIDI Services SDK)
├── nuget.config Registers nuget.org + nuget-local/ as package sources
│
└── tools/
├── DsuDiag/ DSU/Cemuhook diagnostic client
├── Ds4InputDump/ DS4 raw HID input dump (Sony Report 0x01 passthrough debug)
├── vJoy/ Legacy v2 vJoy SDK / test utilities (kept for reference, unused by v3)
├── deploy.ps1 Build + deploy to C:\PadForge
├── deploy_and_restart.ps1 Deploy + restart running instance
├── capture_*.ps1 Wiki screenshot capture (~20 scripts, one per surface)
└── (~50 ad-hoc diagnostic / UI-automation scripts, mostly v2 vJoy holdovers)
| Requirement | Version | Notes |
|---|---|---|
| .NET SDK | 10.0+ |
net10.0-windows10.0.26100.0 target framework |
| Windows SDK | 10.0.26100.57 | Set via WindowsSdkPackageVersion in App csproj |
| Windows | 10/11 x64 | WPF + Windows-specific P/Invoke |
| Visual Studio (optional) | 2022+ | Not required; dotnet CLI suffices |
Minimum supported OS: Windows 10 1809 (build 17763), set via SupportedOSPlatformVersion in the App csproj.
All native DLLs, the HidHide installer, and model assets are checked into the repository. The build needs no external downloads. The Windows MIDI Services SDK installer is fetched from GitHub releases on demand at runtime when the user clicks Install, not at build time.
dotnet build -c DebugOutput: PadForge.App/bin/Debug/net10.0-windows10.0.26100.0/win-x64/
Suitable for development and debugging. Not deployable. Produces loose DLLs and requires the .NET runtime.
dotnet build -c ReleaseOutput: PadForge.App/bin/Release/net10.0-windows10.0.26100.0/win-x64/
Not deployable. Use dotnet publish for deployment.
dotnet publish -c Release
# or explicitly:
dotnet publish PadForge.App/PadForge.App.csproj -c ReleaseOutput: PadForge.App/bin/Release/net10.0-windows10.0.26100.0/win-x64/publish/
| File | Description |
|---|---|
PadForge.exe |
~102 MB, single-file self-contained |
SDL3.dll |
SDL3 native library (content item) |
libusb-1.0.dll |
WinUSB for Switch 2 Pro Controller (content item) |
gamecontrollerdb_padforge.txt |
Custom SDL gamepad mappings (content item) |
Critical: Always use
dotnet publishfor deployment, neverdotnet build. The project is configured for single-file self-contained publish.dotnet buildproduces non-functional multi-file output.
The App csproj defines these publish properties:
<!-- Single-file portable publish: dotnet publish -c Release -->
<PropertyGroup>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<DebugType>embedded</DebugType>
</PropertyGroup>| Property | Value | Effect |
|---|---|---|
RuntimeIdentifier |
win-x64 |
Targets 64-bit Windows only |
PublishSingleFile |
true |
Bundles all managed assemblies into one exe |
SelfContained |
true |
Embeds .NET 10 runtime (no install needed on target) |
IncludeNativeLibrariesForSelfExtract |
true |
Packs native DLLs into the exe. Extracted to temp dir at runtime |
EnableCompressionInSingleFile |
true |
Compresses the bundle to reduce exe size |
DebugType |
embedded |
Embeds debug symbols in assemblies (no .pdb files); preserves stack traces for crash diagnostics |
Note: RuntimeIdentifier is set unconditionally (not only during publish), so dotnet build also targets win-x64 and produces a RID-specific output folder.
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
<WindowsSdkPackageVersion>10.0.26100.57</WindowsSdkPackageVersion>
<RootNamespace>PadForge</RootNamespace>
<AssemblyName>PadForge</AssemblyName>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms> <!-- For NotifyIcon system tray -->
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <!-- Manual AssemblyInfo.cs -->
</PropertyGroup>Key settings:
| Property | Value | Purpose |
|---|---|---|
TargetFramework |
net10.0-windows10.0.26100.0 |
.NET 10 + Windows SDK 26100 (Win 11 24H2). Enables WinRT APIs. |
SupportedOSPlatformVersion |
10.0.17763.0 |
Minimum OS: Windows 10 1809 |
WindowsSdkPackageVersion |
10.0.26100.57 |
Pins Windows SDK NuGet package version |
UseWindowsForms |
true |
NotifyIcon system tray only (not WinForms UI) |
GenerateAssemblyInfo |
false |
Uses manual Properties/AssemblyInfo.cs
|
Nullable |
disable |
Nullable reference types not used |
WinForms implicit using removal. Prevents System.Drawing / System.Windows.Forms ambiguity with WPF types:
<ItemGroup>
<Using Remove="System.Drawing" />
<Using Remove="System.Windows.Forms" />
</ItemGroup><PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<RootNamespace>PadForge.Engine</RootNamespace>
<AssemblyName>PadForge.Engine</AssemblyName>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<DebugType>embedded</DebugType>
</PropertyGroup>The Engine targets net10.0-windows (no specific SDK version needed). Zero NuGet dependencies. All SDL3 and system interop uses raw [DllImport] P/Invoke.
App and Engine share one version via SharedVersion.cs at the repo root. Both csproj files link it in:
<Compile Include="..\SharedVersion.cs">
<Link>Properties\SharedVersion.cs</Link>
</Compile>SharedVersion.cs carries AssemblyVersion and AssemblyFileVersion. The two assemblies cannot drift apart because they compile against the same file. Properties/AssemblyInfo.cs in each project carries the other assembly metadata (title, copyright, COM GUID, theme info) and explicitly does not carry version attributes. Both projects set <GenerateAssemblyInfo>false</GenerateAssemblyInfo> so the build does not regenerate either file.
Important: Edit SharedVersion.cs to bump the version. Never re-introduce AssemblyVersion to Properties/AssemblyInfo.cs. It would override the shared version on whichever assembly carries it and the drift guard breaks. GitHub Releases use git tag names (e.g., v3.3.0) as the user-facing version, but the binary's AssemblyVersion should match.
| Package | Version | Source | Purpose |
|---|---|---|---|
| ModernWpfUI | 0.9.6 | nuget.org | Fluent Design theme for WPF (dark mode, NavigationView, controls) |
| HelixToolkit.Core.Wpf | 2.27.3 | nuget.org | 3D viewport rendering (OBJ model loading, camera, lighting) |
| CommunityToolkit.Mvvm | 8.2.2 | nuget.org | MVVM: ObservableObject, RelayCommand, [ObservableProperty]
|
| Nefarius.HIDMaestro | 1.21.256 | nuget.org | HIDMaestro virtual Xbox and PlayStation controller client API |
| NAudio.Wasapi | 2.2.1 | nuget.org | WASAPI loopback audio capture for bass-driven rumble detection |
| Microsoft.Windows.Devices.Midi2 | 1.0.16-rc.3.7 | nuget-local/ | Windows MIDI Services SDK for virtual MIDI device creation |
No NuGet dependencies. All SDL3 and system interop is via raw P/Invoke.
The Windows MIDI Services SDK is not on nuget.org. The .nupkg is stored locally:
nuget-local/Microsoft.Windows.Devices.Midi2.1.0.16-rc.3.7.nupkg
nuget.config at the solution root registers this folder as a package source:
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="local" value="nuget-local" />
</packageSources>
</configuration>Both nuget.config and nuget-local/ must be present alongside PadForge.sln for dotnet restore to succeed.
Declared as <Content> + CopyToOutputDirectory, which puts them in bin/.../publish/ alongside PadForge.exe during a non-single-file build. With PublishSingleFile=true plus IncludeNativeLibrariesForSelfExtract=true (both set in the csproj) the same files get folded into the single-file bundle and extracted to %TEMP%\.net\PadForge\<hash>\ at first launch. Either way the user never has to drop a DLL alongside the EXE. Single-file deploy is one file.
<Content Include="Resources\SDL3\x64\SDL3.dll" Link="SDL3.dll"
Condition="Exists('Resources\SDL3\x64\SDL3.dll')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Resources\SDL3\x64\libusb-1.0.dll" Link="libusb-1.0.dll"
Condition="Exists('Resources\SDL3\x64\libusb-1.0.dll')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>-
Link="SDL3.dll". Flattens to output root (next toPadForge.exe) instead of preserving the subdirectory path. -
Condition="Exists(...)". Build succeeds even if the DLL is absent (e.g., fresh clone). -
SDL3.dll is a custom fork built from
SDL3-build/SDL/with WinUSB support for Switch 2 Pro Controller. See SDL3 Integration for build instructions. - libusb-1.0.dll provides WinUSB access for Switch 2 Pro Controller communication.
Source location: PadForge.App/Resources/SDL3/x64/
<Content Include="gamecontrollerdb_padforge.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>Custom SDL gamepad mapping database with PadForge-specific entries (e.g., DualShock 3 via DsHidMini SDF). Loaded by SDL3 at runtime. Community mappings can be submitted via the GitHub issue template.
Source location: PadForge.App/gamecontrollerdb_padforge.txt
Compiled into the assembly. Not visible as separate files in the output.
<EmbeddedResource Include="Resources\HidHide_1.5.230_x64.exe" />| Resource | Purpose |
|---|---|
HidHide_1.5.230_x64.exe |
HidHide installer. Hides physical controllers from other applications |
The HIDMaestro user-mode driver is not managed by DriverInstaller. The driver binaries, INF, profiles, and signing tools all ship inside HIDMaestro.Core.dll (referenced as a <Reference>, so it's bundled into the single-file EXE). The engine calls HMContext.InstallDriver() on first start, which registers the driver with Windows through pnputil from inside PadForge's already-elevated process. No separate installer EXE. The OpenXInput shim (xinput1_4.dll only) ships as <Content> and is bundled into the single-file EXE via IncludeNativeLibrariesForSelfExtract. devobj.dll is deliberately not shipped. See Driver Installation Internals for why a bundled stub would crash HID class enumeration. The Windows MIDI Services SDK is downloaded from the GitHub releases API on demand when the user clicks Install, then run with /install /quiet /norestart.
PadForge v2's vJoy and ViGEmBus installers are no longer bundled. v3 detects either driver on first launch and offers to uninstall via the legacy driver cleanup dialog. See Driver Installation Internals.
<!-- 3D controller model assets (adapted from Handheld Companion, CC BY-NC-SA 4.0) -->
<EmbeddedResource Include="3DModels\**\*.obj" />OBJ mesh files for 3D controller visualization. Loaded at runtime via ControllerModelBase.LoadModel().
| Directory | Contents |
|---|---|
3DModels/XBOX360/ |
31 OBJ files (Xbox 360 controller parts) |
3DModels/XBOXONE/ |
46 OBJ files (Xbox One parts, shared with Elite / Series / Adaptive) |
3DModels/DS4/ |
36 OBJ files (DualShock 4 controller parts) |
3DModels/DualSense/ |
48 OBJ files (DualSense parts, Touchpad split for click-mapping) |
<!-- Web controller frontend assets (embedded, served by WebControllerServer) -->
<EmbeddedResource Include="WebAssets\**\*" />HTML/CSS/JS for the browser-based virtual controller. Served by the embedded Kestrel web server at runtime.
| File | Purpose |
|---|---|
WebAssets/index.html |
Landing page |
WebAssets/controller.html |
Virtual gamepad UI |
WebAssets/css/controller.css |
Gamepad styling |
WebAssets/js/controller_client.js |
WebSocket client logic |
WebAssets/js/nipplejs.min.js |
Virtual joystick library |
<EmbeddedResource Update="Resources\Strings\Strings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>Resource files for multilingual UI support:
| File | Language |
|---|---|
Strings.resx |
English (default/fallback) |
Strings.de.resx |
German |
Strings.es.resx |
Spanish |
Strings.fr.resx |
French |
Strings.it.resx |
Italian |
Strings.ja.resx |
Japanese |
Strings.ko.resx |
Korean |
Strings.nl.resx |
Dutch |
Strings.pt-BR.resx |
Brazilian Portuguese |
Strings.zh-Hans.resx |
Simplified Chinese |
The default Strings.resx uses PublicResXFileCodeGenerator to produce Strings.Designer.cs for strongly-typed access via Strings.PropertyName. Satellite .resx files follow standard .NET localization conventions and compile into satellite assemblies automatically.
<!-- 2D controller model assets (Gamepad-Asset-Pack by AL2009man, MIT license) -->
<Resource Include="2DModels\**\*.png" />PNG overlay images for 2D controller schematic view (DS4/, DualSense/, XBOX360/, XBOXONE/, XBOXSERIES/). Uses Resource (not EmbeddedResource) so they load as WPF pack URIs (pack://application:,,,/2DModels/...).
<Resource Include="Resources\Xbox Series Controller - Front.png" />
<Resource Include="Resources\Xbox Series Controller - Top.png" />
<Content Include="Resources\PadForge.ico" />| Resource | Purpose |
|---|---|
| Controller PNGs | Mapping UI images |
PadForge.ico |
Application icon (via <ApplicationIcon> in csproj) |
Resources/ControllerIcons.xaml |
XAML vector icon definitions (resource dictionary) |
Workflow: .github/workflows/build.yml
on:
push:
branches: [v3-dev]
pull_request:
branches: [v3-dev]
workflow_dispatch:Runs on every push/PR to v3-dev and on manual trigger.
-
Checkout.
actions/checkout@v4,fetch-depth: 0(full history for commit counting) -
Setup .NET.
actions/setup-dotnet@v4,dotnet-version: 10.x -
Publish.
dotnet publish PadForge.App/PadForge.App.csproj -c Release -
Upload artifact. Publish directory as
PadForge_r{COMMIT_COUNT}@{COMMIT_SHORT}
On push (not PR), the workflow creates two GitHub releases keyed off the branch ref name:
| Release | Behavior |
|---|---|
archive-v3-dev |
Accumulates every build as PadForge_r{N}@{SHA}.zip. Uses --clobber for latest upload. Preserves tag across builds. |
latest-v3-dev |
Recreated on every push (old release deleted first). Contains PadForge.zip with the most recent build. The "always current" download link. |
Both are marked --prerelease and cross-link to each other in their notes.
Format: PadForge_r{COMMIT_COUNT}@{7-char COMMIT_SHA} (e.g., PadForge_r342@f35bb36)
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: trueCreated manually via gh release create with a version tag (e.g., v3.3.0, v3.3.0-beta1). See Release Workflow below.
PadForge is portable. No installer required.
Copy the publish output to any folder:
cp PadForge.App/bin/Release/net10.0-windows10.0.26100.0/win-x64/publish/PadForge.exe C:\PadForge\PadForge.exeRequired companion files (placed automatically by dotnet publish):
| File | Required | Purpose |
|---|---|---|
SDL3.dll |
Yes | SDL3 native library |
libusb-1.0.dll |
Yes | WinUSB for Switch 2 Pro Controller |
gamecontrollerdb_padforge.txt |
Optional | SDL3 gamepad mappings (enhances device recognition) |
tools/deploy.ps1 # Build, publish, copy to C:\PadForge
tools/deploy_and_restart.ps1 # Deploy + kill running instance + restartPadForge creates PadForge.xml alongside the executable to store settings, mappings, and profiles.
PadForge always requests administrator privileges on startup (declared in app.manifest as requireAdministrator). HIDMaestro / HidHide / MIDI Services management runs inside the already-elevated process.
- Edit
SharedVersion.csat the repo root. UpdatesAssemblyVersionandAssemblyFileVersionfor both App and Engine in one place.
dotnet publish -c Releasecp PadForge.App/bin/Release/net10.0-windows10.0.26100.0/win-x64/publish/PadForge.exe C:\PadForge\PadForge.exeRun C:\PadForge\PadForge.exe and verify functionality.
git add -A
git commit -m "Release vX.Y.Z"
git pushcd PadForge.App/bin/Release/net10.0-windows10.0.26100.0/win-x64/publish
zip -r PadForge-vX.Y.Z-win-x64.zip .gh release create vX.Y.Z --title "PadForge vX.Y.Z" --notes "Release notes here"
gh release upload vX.Y.Z PadForge-vX.Y.Z-win-x64.zipUse --prerelease for beta/RC releases. Use --latest for the default download.
Standalone diagnostic utilities and development scripts.
| Tool | Command | Framework | Dependencies | Purpose |
|---|---|---|---|---|
| DsuDiag | cd tools/DsuDiag && dotnet run |
net8.0 | None | Real-time DSU/Cemuhook UDP client for verifying gyro/accel axis mapping |
| Ds4InputDump | cd tools/Ds4InputDump && dotnet run |
net8.0-windows | None (raw HID) | Dumps DS4 raw HID input frames. Used to validate Sony Report 0x01 passthrough on PlayStation virtual controllers |
The v2 vJoy/Test and vJoy/FfbTest utilities are still in the repo under tools/vJoy/ for historical reference. They target the deprecated vJoy stack and are not part of the v3 toolchain.
| Script | Purpose |
|---|---|
deploy.ps1 |
Build + publish + copy to C:\PadForge
|
deploy_and_restart.ps1 |
Deploy + kill running instance + restart |
capture_*.ps1 |
~20 per-surface wiki screenshot scripts (e.g. capture_all.ps1, capture_pad_*.ps1, capture_macros.ps1, capture_3d_2d.ps1, capture_3d_web.ps1, capture_extra_slots.ps1). One per major UI surface; capture_all_wrapper.ps1 runs them in sequence. |
A handful of *vjoy* scripts (cleanup_vjoy.ps1, check_vjoy_state.ps1, etc.) are v2 holdovers kept for diagnosing legacy installs the cleanup dialog encounters. The remaining ~50 scripts are ad-hoc UI automation and screenshot utilities.
| Issue | Cause | Fix |
|---|---|---|
SDL3.dll not found at runtime |
DLL not in Resources\SDL3\x64\
|
Download from SDL3 releases or build from fork |
| MIDI package not found |
nuget.config missing or nuget-local/ folder missing |
Ensure both are present at solution root |
| Missing WPF types | Using dotnet build instead of dotnet publish for deployment |
Always use dotnet publish -c Release for deployment |
System.Drawing ambiguity |
WinForms implicit usings conflict with WPF | Ensure <Using Remove="System.Drawing" /> is in csproj |
| HelixToolkit errors | Package restore failed | Run dotnet restore first |
| Large exe size (~102 MB) | Expected. Self-contained with bundled .NET runtime |
EnableCompressionInSingleFile already enabled |
| CI build fails | .NET 10 SDK not available in runner | Check actions/setup-dotnet version supports .NET 10 preview |
HIDMaestro.Core reference fails to resolve |
Resources/HIDMaestro/HIDMaestro.Core.dll missing or wrong build |
Drop a Release-build HIDMaestro.Core.dll from a tagged HIDMaestro release into Resources/HIDMaestro/
|
- Architecture Overview: Solution structure, dependencies, design philosophy
- Engine Library: Shared data types, SDL3 P/Invoke, device wrappers
- Settings and Serialization: XML persistence, data models
- SDL3 Integration: Custom SDL3 fork, build instructions
- Driver Installation Internals: Driver lifecycle (HIDMaestro, HidHide, MIDI Services) and legacy v2 cleanup
- HIDMaestro Deep Dive: HM SDK surface, OpenXInput shim, four-surface filtering architecture
-
DSU Protocol Implementation:
DsuDiagdiagnostic tool intools/