
3.1.4 / 11-05-2026
Performance
Features
PadForge
Accuracy
Modern fork of x360ce built with SDL3, ViGEmBus, vJoy, Windows MIDI Services, HelixToolkit, .NET 10 WPF, and Fluent Design. Turn any input device into any virtual controller:
Gamepads, joysticks, keyboards, mice, and touchscreens — mapped to Xbox 360, DualShock 4, DirectInput, MIDI, or Keyboard+Mouse output that games treat as real hardware.
Most Recent Changes
--3.1.4--
Highlights
Per-profile 3D and 2D renders. Xbox 360, Xbox One / Elite / Adaptive, Xbox Series, DualShock 4, and DualSense each have their own native artwork. Assigning a profile swaps the model and overlays to match.
Xbox Series Share button. Clickable on the 3D mesh, in the 2D overlay, and in the mapping grid (also recorded by Map All). Non-Series Xbox profiles use the same Series mesh but leave the Share region inert so it stays visually accurate without firing on press.
DS4 lightbar parity with DualSense. Audio modes, base modes, and macro override all now route through the DS4 effect synthesizer the same way they do on DualSense.
DS4 force feedback fix. Gates the rumble-skip on active DualSense passthrough so DS4 virtuals always rumble.
SideWinder force feedback hotfix. Per-RID magnitude scale, descriptor-aware Set Effect / Set Periodic decoding, and a gate that stops Microsoft-VID Xbox-rumble branches from stealing PID FFB.
HIDMaestro v1.3.5 → v1.3.12. Custom Extended trigger classifier fix, HMGamepadState.Axes dict migration, layout-derived per-row axis map, plus the SideWinder profile + FFB hotfix chain.
DualSense 3D touchpad finger preview is now cropped to the real ~52x32 mm touch surface. Previously the finger sphere drifted past the visual edges and stopped well above the bottom because the inset constants were tuned for the smaller DS4 Screen mesh.
Web controller trigger z-order fixed. The trigger-base silhouette now sits behind the body PNG and the active press-fill draws in front, matching the desktop ControllerModel2DView. Xbox 360 and DualShock 4 web layouts now show the trigger bulges above the bumpers and the press fill animates correctly.
WPF-UI 4.3.0, click-outside clears keyboard focus at the window level, and diagnostic logs stripped across HM/FFB/DS5/Sony so only crash.log remains in normal operation.
Visualization
Per-profile 3D and 2D renders: DualSense, Xbox One S, and Xbox Series X (white) staged alongside the existing Xbox 360 and DualShock 4 artwork (
a409b30).Per-profile 2D/3D asset routing wires the new artwork into the model picker (
3a4c159).Xbox One/Series bumper position fix + DualSense touchpad preview added to the 2D layout (
0298dd3).Xbox One/Series press-overlay sizing tuned + synthesized DualSense touchpad mesh (
fb14a79).Visual analysis pass for Xbox 2D layout sizing + real DualSense touchpad mesh (
729aa70).Xbox Series LB/RB and d-pad tuned, DualSense bbox-fit + 3D scale, white touchpad (
784fa00).DualSense 3D scale moved to the parent level + Xbox Series d-pad edge anchoring (
8eaf6fd).3D left-drag rotation restored, stick quadrants now use the mesh centroid (
b52bcfa).Quadrant wedge, flash arrow, and ring overlay also use the mesh centroid for consistent hit-testing (
29e5874).Start/Back/Bumpers snapped to actual rendered positions on each base PNG (
074c6dd).Xbox 360 triggers clipped above bumpers, Xbox One bumper highlight widened (
d5e13de).Xbox 360 triggers flushed to bumper top, Xbox One bumpers widened further (
a2abd4d).Visual analysis fix pass: Xbox One bumper width 0.27, drop the Xbox 360 trigger clip that was over-cropping (
781baca).Xbox One bumper width tuned + Xbox 360 trigger / stick fits (
419ee9f).2D overlay polish: per-controller trigger and stick canonical positions (
83f4971).
Xbox Series Share button
Share button wired end-to-end: HMButton.Share dispatch, gamepad source field, mapping grid row gated to xbox-series-* profiles, Map All opt-in, and 2D + 3D visual feedback. Non-Series Xbox profiles render the same mesh but leave the Share region inert (
f2ffc32).
Adaptive TriggersLighting
DualSense / DS4 lightbar and rumble
HIDMaestro v1.3.5 migration brings new lightbar and mic-LED features; Sony rumble fix lands in the same wave (
1eb3327).Sony dispatcher fixes + profile-filter tightening + diag capture path (
58b465a).HIDMaestro v1.3.5 final release build integration (
bf0ab5c).Dead lightbar reset commands and strings removed after the per-channel UI rework (
4c83af5).DS4 virtual FFB gates the rumble-skip on active DS5 passthrough so DS4 virtuals always rumble (
9d4142d).
SideWinder / PID force feedback
PID FFB detection fixed for descriptors with non-adjacent Usage Page (
148bb45).Microsoft-VID Xbox-rumble branches no longer steal SideWinder PID FFB (
a4e9898,8acf41b).SideWinder FFB diagnostic: per-packet dispatch tag + Connect-time ffb-decoder log (
dc27d75).Decode SideWinder's short Set Effect / Set Periodic / no-Set-Effect Start path (
8fd564e).DecodeSetEffect: only read Gain/Direction at canonical offsets when len >= 21 (
400e9c4).PID FFB decoder parses descriptor for per-report magnitude scale (
b068de9).Per-RID magnitude format for periodic-style FFB reports (
b165cce).FfbTest expanded: Square / Triangle / Saw / conditions / Ramp coverage (
a17c1a3).FfbTest condition crash fixed + decoder ramp-on-periodic-RID misroute fixed (
1605a7e).
HIDMaestro integrations (1.3.5 → 1.3.12)
v1.3.6: Custom-shaped trigger classifier fix (
a85a5b9).v1.3.7 (
cfcd35d).v1.3.8 (
bef47e5).v1.3.9: HMGamepadState.Axes dict migration on the PadForge side (
b57d938).v1.3.10: SideWinder profile hotfix (
ad10b43).v1.3.11: SideWinder force feedback hotfix (
cf7c8cc).v1.3.12: SideWinder force feedback hotfix (
d982604).
Custom Extended layout
Use HMProfile.StickCount / TriggerCount for the Extended row count (
75885b7).Layout-aware Extended row count prefers HMProfile.Layout over the classifier (
7b1c6f8).Layout-derived per-row axis map for Custom Extended writes (
34fb4fc).Revert layout-wrapper helpers; use HMProfile.Sticks/Triggers directly (
c349a1b).Custom Extended triggers: pressed-wins merge + released-rest default (
02945f6).
UI / framework
WPF-UI bumped to 4.3.0 (
1edfa0f).Click-outside clears keyboard focus at the window level (
11f7a8d).PadPage: sectional Reset All + consistent tooltip casing (
ac5d097).
Diagnostics
Diagnostic logs stripped across HM/FFB/DS5/Sony; only crash.log remains in normal operation (
82534cb).
Release plumbing
v3.1.4: bump version, refresh screenshots, README per-profile renders blurb. Adds capture_v3_1_4 + capture_v3_1_4_fix tooling for additive screenshot recapture (
c99ba7e).
--3.1.3--
Per-device constant force, rumble macro actions, and a touchpad click bar. New continuous-force override on the Force Feedback tab with a 2D X / Y grid, two new rumble actions for the macro editor, a hold-to-click strip on the in-app touchpad overlay, and a stack of bug fixes spanning web controllers, FFB pass-through, and the test-rumble dispatch path.
Force Feedback — Constant Force per device (#29)
The Force Feedback tab now has a Constant Force card under the rumble card. A toggle plus a 200×200 X / Y grid drive a continuous force on the assigned physical device. Click or drag inside the grid to set the direction and strength of the force vector — center is no force, the edges are full strength, the angle of the dot from center sets the polar direction.
The force runs while the toggle is on, except when a game or program is sending its own non-zero force to that device-slot pair — game force always wins while it's active, then the constant force resumes the moment the game returns to silence (override-with-resume). Macro rumble layers via max() ahead of constant force, so a macro pulse behaves the same as game force for resume purposes.
Routing per device class:
FFB-capable devices (wheels, joysticks) receive a real
DICONSTANTFORCEthrough the existingSetDirectionalHapticForcespipeline. Single-axis devices (wheels) project the angle onto the steering axis exactly like the DXSDK FFConst sample.Rumble-only devices (Xbox-style pads, generic gamepads) get a quadrant motor mapping:
|Y|drives the heavy / low-frequency motor,|X|drives the light / high-freq motor, with a half-bleed across so diagonals engage both.Sony pads (DualShock 4 / DualSense / DualSense Edge) route through the
UserEffectsDispatcherper-device rumble pump — the dispatcher remains the sole writer of effect packets to Sony pads, so this layers on top of game rumble and audio rumble without reintroducing any second writer.
Persists across PadForge restarts, per-device per-slot. The Motor Activity meter on the FFB tab reflects the constant force when it's the active source — what you see on the meter is what the device receives. Solves the wheel-mapped-to-virtual-Xbox-needs-centering case that's been tracked since the v2 era.
Macros — Rumble and Stop Rumble actions (#62)
Two new action types in the macro editor, parallel to the existing Lightbar Color / Lightbar Color Clear pair:
Rumble. Drives the slot's physical device rumble from a macro. Per-motor strength sliders (0–100% each, set one to zero to fire one motor in isolation) plus two hold modes: Reactive runs at full strength across the configured Hold window then linearly fades to zero across the Fade window (good for shoot / hit / confirm pulses), or Sticky holds at full strength until a Stop Rumble action runs (good for armed / engaged states).
Stop Rumble. Releases any active rumble override on the slot. Pair with a Sticky Rumble action to release a held rumble from another macro.
Layers over game-driven rumble via max() so user-driven feedback always reaches the motors. Sony slots route through the same dispatcher that drives game rumble (sole-writer model preserved); non-Sony slots take the SDL haptic / rumble path. Localized across all 10 shipped locales.
Touchpad overlay — hold-to-click bar
The in-app touchpad overlay (toggled by the ToggleTouchpadOverlay macro action) now has a 32 DIP click strip along the bottom of the window. Mouse-press or touch-hold sets state.TouchpadClick = true for as long as you hold, so click-drag patterns work — the overlay's existing surface double-tap pulse is momentary and could not. The strip has no label so it doesn't obscure whatever the overlay is sitting on; the semi-transparent tint is the affordance, brighter while held.
The overlay's legacy click path also moves off state.Buttons[20] (a v2-era click-as-numbered-button artifact) onto the canonical state.TouchpadClick field, so the overlay's click now auto-maps to PlayStation slots via the same "Touchpad 0 Click" descriptor the rest of the app uses.
Web controllers — layout-specific identity, touchpad routing, dropdown dedup
Several fixes for the browser-driven controller surfaces:
Xbox 360 and DualShock 4 web controllers no longer overwrite each other in the Devices list. The "BT reconnect" fallback in
FindOrCreateUserDevicegated only on ProductGuid + offline state. Both web layouts shipped with the same static ProductGuid, so disconnecting one and connecting the other migrated the offline row's slot mapping onto the new device. ProductGuid is now derived from the layout key (xbox360/ds4/touchpad) so each layout reads as a distinct product.DS4 web controller touchpad click auto-maps to PlayStation slots. The touchpad overlay zone in the controller page sent
kind: button, code: 11(the legacy click-as-numbered-button shape), so a DS4 web controller assigned to a PlayStation virtual controller never auto-mapped the click. Now sendstype: touchpad, click: true/falseso the click lands instate.TouchpadClickand the canonical"Touchpad 0 Click"descriptor resolves at runtime.Mapping dropdown no longer shows touchpad inputs twice.
WebControllerDevice.GetDeviceObjects()was publishing the touchpad finger axes (as bogusAxis 18..23) and a Touchpad Click button alongside the canonical"Touchpad 0 Finger N X / Y / Down"and"Touchpad 0 Click"descriptors. The list now exposes the gamepad-shaped surface only; touchpad inputs come from the canonical block.Button count reads as 11, not 12.
NumButtonsandRawButtonCountno longer add+1forHasTouchpadsince the click rides its own state field.
The standalone touchpad page also gains a dedicated Click button beneath the touch surface so press / release pairs work for click-and-hold.
Bug fixes
Extended FFB toggle now works on customized catalog profiles. Most HIDMaestro catalog profiles ship without a PID FFB block. The customize-rebuild path was only rebuilding the descriptor when disabling FFB, so enabling FFB on a non-
Customprofile left the descriptor FFB-less. The PID FFB packet decoder was also gated on_profile.VendorId == 0xBEEF(the synthetic Custom profile's VID), so customized catalog profiles couldn't run the decoder even after a rebuild. Both fixed: the rebuild always carries the FFB block when the toggle is on, and the decoder gates on whether the descriptor itself contains the canonical PID FFB block signature instead of on VID.Tab visibility refreshes after in-place device-list swap. With multiple devices on a slot, unassigning the selected one slid the next device into the same
MappedDeviceInfoobject via in-place mutation, soSelectedMappedDevicePropertyChanged didn't fire and the FFB / Lighting / Adaptive Triggers tabs stayed pinned to the previous device's capabilities until the user manually toggled the dropdown.Test rumble on DS5 / DS4 stops at 500 ms instead of 6–8 s. The dispatcher's polling-thread stop path in
UpdateAnimTimerwas callingStopAnimTimer()without first dispatching a final snapshot. When the test-rumble clear timer zeroed the motors, the controller never received a "rumble = 0" packet and kept rumbling until something else triggered a dispatch. Now mirrors the OnAnimTick early-exit's final-snapshot behavior on the polling-thread stop path.PlayStation 3D touchpad finger map raised at the bottom edge.
zBottomInsetFracbumped from 0.08 to 0.12 (about 3.9 mm above the mesh's bottom edge instead of 2.6 mm) so a touch atnormY = 1lands where a real DS4 v2 finger sits at the bottom of the touch surface. Top boundary unchanged.PadPage tab and card icons refreshed. Force Feedback tab + Rumble card now use Segoe MDL2
E877(Vibrate). Constant Force card usesF0AD. Lighting tab usesE781(Lightbulb). Indicator Lights card usesEF31. Replaces the custom inlinePathdata that used to ship for those tabs.
Localization
All new strings (Constant Force card on the FFB tab, rumble macro action editor) localized across the 10 shipped locales: de, es, fr, it, ja, ko, nl, pt-BR, zh-Hans. The rumble hold-mode dropdown values mirror the lightbar Reactive / Sticky wording per-locale so the two action families read consistently.
--3.1.2--
Adaptive Triggers — Load GameCube preset (#78)
When the active mode on a DualSense / DualSense Edge slot is set to Weapon, a row labeled "Load GameCube preset" appears below the Mode dropdown. Clicking it loads:
Start = 0x90 (≈ 56 % of trigger pull)
End = 0xA0 (≈ 63 % of trigger pull)
Strength = 0xFF (max force)
The byte values match the GameCube presets shipped by Mxater's DualSenseSupport and DualSenseY-v2. The synthesizer's Weapon branch passes the slider values directly through to bytes 1/2/3 of the per-trigger effect block, so what the firmware applies matches the verified click-feel of a real GameCube trigger: smooth pull through ~56 % of travel, resistance ramp from 56 % to 63 %, hard wall past that with maximum force.
It's a one-click loader, not a lock — the sliders stay editable afterwards, so you can tweak any of the three values to taste.
For a full hybrid analog-then-digital trigger workflow (firmware click resistance from this preset, plus a digital button activation past the click point), pair this with the trigger ceiling control on the Triggers tab and an axis-to-button-deadzone mapping on the Mappings tab.
Localized across all 10 shipped locales (de / es / fr / it / ja / ko / nl / pt-BR / zh-Hans).
--3.1.1--
Lighting tab — per-(slot, device)
Two physical Sony pads on the same virtual controller slot can now have different lightbar modes, different palettes, different Input Reactive variants. The Lighting tab follows whichever device is selected in the assigned-devices dropdown, exactly the way the Mappings, Sticks, Triggers, and Force Feedback tabs already worked.
Macro lightbar actions stay slot-level — a macro that switches the lightbar to Rainbow fans out across every Sony pad on the slot, each rendering Rainbow with its own configured period and brightness.
Input Reactive overlay
The three Input Reactive variants (random hue, palette cycle, fixed color) are now a separate overlay dropdown rather than base lightbar modes. Pick any base — Static / Rainbow / AudioPulse / Off — and layer Input Reactive on top. Each button press flashes the reactive color at full intensity and lerps back to the base across the configured Hold + Decay window.
The fixed-color variant has its own RGB picker, separate from the base color, so a Static-blue base + white per-press flash is one of many combinations that previously couldn't coexist.
Migration is automatic: old LightbarMode = InputReactive* saves migrate to LightbarMode = Off + the corresponding overlay variant; the visual result is unchanged.
Macro lightbar actions (#63)
Four macro action types for lightbar control:
LightbarColor — push an RGB triple as a temporary override on every Sony pad on the slot. Two hold modes:Reactive — flash at full intensity, decay over the configured fade-out.
Sticky — hold the color until the next clear or another macro override.LightbarColorClear — release any active macro override, restoring the configured base mode.
LightbarModeSet — set the slot's base lightbar mode (with Input Reactive variants auto-translating into the new overlay).
LightbarModeCycle — step through a configured list of base modes per macro fire.
Macro overrides beat both the base mode and the Input Reactive overlay; game-driven writes still win at packet level via the existing passthrough path.
DualShock 4 audio lightbar (#55)
DS4 picks up the same audio-driven lightbar logic DualSense had: Audio Pulse (static / random / rainbow), Audio Bands (hard / smooth / crossfade), and the Input Reactive overlay all run on DS4 too, with full USB Report 0x05 + BT Report 0x11 + CRC32 packet generation through the dispatcher's raw-HID path.
Audio rumble — sole-writer architecture
The audio-rumble + animated-lightbar interaction in v3.1.0 produced a 30 Hz motor stutter that users perceived as weak rumble. The cause was two simultaneous writers (PadForge's effect-packet dispatcher and SDL3's SDL_RumbleJoystick) competing through separate HID handles on an asynchronously-sampled audio peak — they always disagreed at sample time, and the firmware applied "whichever WriteFile lands most recently."
v3.1.1 replumbs DS5/DS4 rumble around a single writer. InputManager.Step2.ApplyForceFeedback now skips Sony VID 0x054C / DS5 / DS4 PIDs entirely; UserEffectsDispatcher writes the entire effect packet (rumble + lightbar + adaptive triggers + mic LED) every 33 ms, with rumble bytes computed per-device via the same audio-mix + gain math SDL used to do. One writer, no race.
A polling-thread "rumble poke" keeps the dispatcher's animation timer alive whenever audio rumble is enabled or game rumble is in flight, even with a static / off lightbar — so audio rumble works the same with or without an animated mode running concurrently.
Per-(slot, device) Force Feedback
Different physical Sony pads on the same slot can now carry different gain settings — the FFB tab follows the selected device, same as Lighting. Test rumble routes only to the selected device instead of fanning out across every Sony pad on the slot.
Keep devices cloaked between launches (#75)
New Settings toggle under the existing "Hide devices from games" master switch. Off by default (PadForge clears its HidHide cloaks at shutdown). On: cloaks persist between PadForge sessions, so processes scanning for controllers while PadForge is closed (Steam launching after PadForge exits, for instance) still see the physicals as cloaked. The runtime master toggle is unchanged — flipping it off mid-session still decloaks immediately.
Other fixes and polish
Slot delete now clears the per-device Lighting configs in the deleted slot's PadViewModel, so re-mapping the same physical device to a freshly-created slot starts from defaults instead of resurrecting the old slot's lightbar mode.
Settings save reliability: per-device Lighting settings used to bleed across devices on the slot due to a load-time fan-out; v3.1.1 only fans out for genuine pre-v3.1 saves with no per-device entries. Lighting tab edits now persist correctly across app restarts.
Touchpad click participates in Input Reactive — pressing the DS5 touchpad now fires a reactive pulse, alongside the standard button presses.
Inactivity timeout label cleaned up — was "Inactivity destroy timeout", now just "Inactivity timeout"; trailing colons stripped from Polling interval / Inactivity timeout in all 10 locales (matches the existing pad-config-tab style).
Localization pass: 10 new strings translated across de / es / fr / it / ja / ko / nl / pt-BR / zh-Hans for the Input Reactive overlay, the per-press flash color, and the cloaks toggle.
Code audit: 40 build warnings cleared (CS0168 / CS0108 / CS0420 / CS9191 / CA2014). No behavioral changes — exception-variable cleanup, private-const renames that stop shadowing inherited members, redundant
volatilekeyword drops on fields that already useVolatile.Read/Write, anin-arg modifier fix on aMarshal.QueryInterfacecall, and a stackalloc hoisted out of a per-slot loop.
--3.1.0--
DualSense, fully wired.
Two new tabs on PlayStation-class slots with a DualSense or DualSense Edge assigned: Adaptive Triggers (seven effect modes with a live profile-preview graph) and Lighting (thirteen lightbar modes plus a separate Indicator LEDs card for the player row, mute LED, and brightness). Game-driven trigger and rumble effects forward through automatically; user settings apply when no game is writing. Plus a HIDMaestro bump, a tab UI pass that retints with light/dark, and 128 new strings localized across all nine non-English locales.
Adaptive Triggers tab
Visible only on slots with a DualSense or DualSense Edge assigned. Each trigger gets a mode dropdown, a live effect-profile graph that draws the resistance or amplitude shape across the trigger pull, Range / Strength / Frequency sliders, and per-parameter reset buttons.
Seven modes, mapping directly to the firmware's ScePadTriggerEffectParam types:
ModeWire opcodeShapeOff0x00Empty trackFeedback0x01Solid bar from start position to fully pressedWeapon0x02Resistance through the soft zone, click at endVibration0x06Continuous sine wave from start onwardMulti-Position Feedback0x21Alternating bumps inside [start, end]Slope Feedback0x26Triangular ramp from start to end, held past endMulti-Position Vibration0x25Stuttering sine bursts inside [start, end]
The 0x21 / 0x25 / 0x26 opcodes use Sony's official 10-zone bitmap encoding. The preview graph re-renders as you drag the sliders, so what you see is what the firmware will receive.
Game-driven trigger effects pass through unchanged via a separate dispatch path. The user-settings layer fires only when no game is driving the trigger.
Lighting tab
Visible only on slots with a DualSense or DualSense Edge assigned. Thirteen lightbar modes:
ModeBehaviorOff (Game Owns the Lightbar)Default. Only game writes drive the lightbar.Static ColorSingle user color.BreathingSingle color, sinusoidal fade.Rainbow CycleHSV sweep across the full hue wheel.Color CycleSteps through the configured palette; smooth-blend toggle.Audio Pulse — Static ColorBrightness modulates with system audio peak.Audio Pulse — Random Color per BeatNew hue on each beat detected.Audio Pulse — Rainbow CycleRainbow with audio-driven brightness.Audio Bands — Three Colors, Hard TransitionsQuiet / Medium / Loud, instant switch at thresholds.Audio Bands — Three Colors, Smooth GradientLinear interpolation between thresholds.Audio Bands — Three Colors, Crossfade at BoundariesConfigurable crossfade width.Input Reactive — Random Color per PressEach button press flashes a fresh hue, decaying linearly.Input Reactive — Cycle Through PaletteEach press steps to the next palette color.
Variable-length palette with per-entry color picker (hex + RGB sliders + reset), per-band thresholds, crossfade width, sensitivity, pulse decay. A separate Indicator LEDs card covers the player-pattern row, mute LED mode, and brightness. Every dropdown in the tab has a one-click reset button to its default.
Lightbar packets ship via raw HID using OpenRGB's packet shape, not SDL3's joystick rumble path. Bit 5 of byte 43 (the no-fade flag) is what makes the firmware skip its boot/connect fade and apply the requested state immediately. Without it, late-connect packets visually lose to the firmware's default state.
Game-driven passthrough
Two paths cover the DualSense / DualSense Edge surface:
Game → physical device. Trigger effects, rumble, and lightbar writes from the running game forward to the assigned source device exactly as the game intended, with no interpretation or remap.
User settings → physical device. Adaptive Triggers and Lighting tab settings apply only when the game isn't writing to that specific surface (per-trigger and per-lightbar). User and game layers don't fight.
Player pattern, mute LED, and brightness write independent of the lightbar, so a "lightbar off but show me the player number" config works.
HIDMaestro 1.3.4
Bumped from 1.3.2 (shipped with v3.0.7). Picks up upstream catalog updates and minor PnP-walk hardening. No PadForge-side behavior change required.
Tab UI overhaul
Page-header + bordered sub-section style for Sticks / Triggers / Force Feedback / Adaptive Triggers / Lighting, matching the Settings and Dashboard surfaces. Tab-strip glyphs replaced with DrawingImage SVG paths so the icons retint cleanly on a light/dark switch. New labeled-stick / labeled-trigger icons by Zacksly (CC BY 4.0, attribution in Resources/ZackslyIcons/ATTRIBUTION.txt).
The Extended schematic preview now uses SetResourceReference for its brushes, so a theme switch retints the schematic instead of leaving it stuck on whatever theme was active when the slot was created.
Localization
128 new Adaptive Triggers + Lighting strings, fully translated across de, es, fr, it, ja, ko, nl, pt-BR, zh-Hans. Each locale matches its native convention rather than mirroring English Title Case verbatim: German keeps noun capitalization, French / Italian / Spanish / Dutch / Brazilian Portuguese drop to sentence case for body labels, Japanese / Korean / Simplified Chinese have no case to apply.
Brazilian Portuguese also got a Title Case pass on its setting titles to match the rest of the app: Brilho do LED:, Padrão do Jogador:, Modo do LED de Mudo:. Gatilhos Adaptativos (tab name + page header) and Force Feedback (tab vs. page header consistency) corrected too.
Other fixes and polish
Slot delete clears macros. Previously, deleting a virtual controller and creating a new one in the same slot left the deleted slot's macros attached to the new VC. Macros are bound to the slot, not the physical device, so they now clear on delete.
Devices page ordering. Device badges and the slot selector follow the type-group order (Xbox group → PlayStation → Extended → KbM → MIDI), not slot-creation order, so reorders don't shuffle the visible numbering.
Audio capture gating. WASAPI capture is gated on
LightbarModerather than the legacy "audio rumble enabled" bool, so audio modes start and stop cleanly without depending on Force Feedback state.PlayStation config persistence. AT and Lighting settings persist across slot teardown, so reconnecting the same physical DualSense doesn't lose the configuration.
DS5 reconnect. Per-device retry burst extended to 15 s to cover the BT LED reset window on initial pairing.
--3.0.7--
Xbox slot teardown — 5,700 ms → 135 ms (HIDMaestro 1.3.1)
DeviceManager.RemoveDevice previously tore down HID children before closing the SwD parent. Children of an SwD parent can't fully unwind while the parent's HSWDEVICE refcount is held, so each WaitForDeviceRemoval call hit its 2,000 ms timeout and the cumulative budget for an Xbox 360 wired teardown landed near 5.7 s.
HM 1.3.1 closes the SwD parent first and blocks on CM_NOTIFY_ACTION_DEVICEINSTANCEREMOVED for the parent. The parent close takes ~55 ms and cascades the children automatically — net ~135 ms. Phantom-only registry residue (entries with no live device) skips the hmswd.exe round-trip entirely, saving ~50-75 ms per stale entry and preventing creep across same-process recreations.
This change carried through PadForge v3.0.6 and is the foundation the v3.0.7 create-side fix builds on.
Xbox slot-claim wait cap — 15 s → 500 ms (HIDMaestro 1.3.2)
The XInput slot-claim wait inside HM's SetupController was budgeted at 15 s. That sounds defensive until you realize the slot-claim distribution is bimodal:
Working case: the new slot publishes in under 100 ms. Typical: ~65 ms for Xbox 360 wired, ~3-10 ms for Xbox Series BT.
Stuck case: kernel state issue, prior-session residue, or
xinputhidrecovering from a recent unbind. The slot never publishes, and the old budget sat through the full 15 s before logging the failure.
So the budget had no working-case payoff and only ran to completion on stuck-allocator failures. PadForge users observed 13-14 s freezes on a single Xbox Series BT create when this hit.
HM 1.3.2 caps the wait at 500 ms — about 5× the slowest healthy claim observed. Slow-but-working creates still finish well inside their headroom; the stuck case degrades to a near-imperceptible pause that gets logged and falls through to ordinary creation. Validated empirically against an Atom x5-Z8350 + eMMC stick PC running Windows 10 IoT Enterprise LTSC — both the worst hardware we have on hand and realistically the worst we're likely to ever test against, since anything slower than the Z8350 generation predates the platform's minimum hardware requirements. Xbox VC create, swap, and bubble-down cascade-recreate all feel instantaneous now.
Bubble-down cascade now uniform across Xbox / PlayStation / Extended
Earlier v3.x builds limited the cascade to Xbox slots only and only fired it on full slot deletion or on the HM inactivity timeout (default 60 s). Two gaps:
PlayStation and Extended observe creation order too. DirectInput, SDL, and raw HID all enumerate devices in creation order. A PS or Extended slot at position 1 going non-active without a cascade leaves survivors at positions 2+ holding their original kernel slots — games that bound to "controller N" by enumeration index see the wrong device. Same shape as Xbox via
xinputhid, just a different external observer. The cascade now fires uniformly across all three HM subgroups.Sidebar disable and "all devices unassigned" weren't triggering it. Toggling a slot off via the sidebar power dot, or clearing the slot's mapping panel, would destroy the live VC immediately (no grace period — these are deliberate user actions) but skip the cascade. Surviving slots at higher positions held their kernel slots until the user manually reordered or restarted. Both paths now fire the cascade synchronously alongside the destroy.
Bonus: a position-vs-raw-index bug in the
OnSlotDeletedcascade loop is fixed. Previously, after a like-to-like swap that reshuffled the group's pad-index ↔ position correspondence, deleting a slot could over-destroy lower-position-but-higher-pad-index siblings and miss higher-position-but-lower-pad-index slots that needed to bubble.
The architecture rule (j) added to InputManager.Step5.VirtualDevices.cs documents the all-HM-subgroups cascade scope as the source of truth.
Extended config bar refreshes on profile switch (#73)
ApplyProfile writes the active slot's ExtendedSlotConfig.Customize, OemNameOverride, and ProductString directly when the user reloads a saved profile. The Extended config bar's CheckBox / TextBox controls used an imperative two-way sync that only re-read the model on the slot-load, slot-switch, and OutputType-change paths — none of which fire when an active Extended slot's config gets mutated under it by a profile reload. Engine state was correct (the game saw the right Product String per profile); only the UI display went stale.
PadPage now subscribes to ExtendedSlotConfig.PropertyChanged on the active config instance and re-runs the imperative sync when Customize, OemNameOverride, or ProductString mutates externally. The _syncingExtendedConfig guard prevents the sync from cycling back through the UI events. Count fields (sticks, triggers, POVs, buttons) keep their existing sync — they pull from the active HM profile rather than ExtendedConfig directly and refresh through the ProfileId PropertyChanged trigger.
--3.0.6--
Pulls in HIDMaestro 1.3.0's perf and durability work atop v1.2.2's locale-stable pnputil parsing. No PadForge-side code changes — straight DLL swap and version bump.
What HIDMaestro 1.3.0 changes
Filesystem fast-path on
IsHidMaestroDriverInstalled— skips the full pnputil enumeration when the driver INFs are already in the store.Parallelized
HMContextconstructor prewarm tasks.Skip the GIP-buffer pack/copy in
SubmitStateandSubmitRawReportfor non-Xbox profiles. PlayStation and Extended don't use that surface, so the work was wasted on every frame.Persistent diag and timing-log
StreamWriters (wasFile.AppendAllTextper call).Thread-local reusable buffers in
GetHidChildIdandGetDeviceIdto drop per-call allocations.Tighter
SwdDeviceFactory.Removehmswd timeout (30 s → 8 s).Per-hwId
DeviceOverrideswrite dedup inCreateDeviceNode.WriteBitsbyte-aligned fast path; button-mask bit-pop loop replaces a 32-iteration scan.TimeoutScale.Applyscaling fordevcon WaitForExitbudgets.Driver INF version bump.
The result on PadForge's side is faster slot create/destroy and lower CPU during steady-state polling, particularly when many HM virtuals are active simultaneously.
--3.0.5--
"Start at login" toggle now actually works
Since v3.0.0 began requiring elevation (auto-elevation for vJoy / HIDMaestro driver lifecycle), the Settings → "Start at login" toggle silently no-op'd. The prior implementation wrote HKCU\Software\Microsoft\Windows\CurrentVersion\Run, which Windows can't use to launch elevated apps — there's no way to show a UAC prompt at the login screen, so the entry just gets dropped.
PadForge now registers a Task Scheduler task with /SC ONLOGON /RL HIGHEST. Task Scheduler is a trusted launcher so it can start elevated apps at logon without user interaction. Driving schtasks.exe strictly by exit code — no parsing of human-readable output — keeps it locale-independent (same lesson as the v3.0.4 hotfix).
Existing users who had the (broken) HKCU\Run toggle on under v3.0.0 through v3.0.4 get auto-migrated on first launch of v3.0.5: PadForge spots the legacy registry entry, creates the working scheduled task in its place, and removes the old entry. No re-toggle needed.
Two same-model Xbox Series Bluetooth pads no longer merge into one card
Pairing two Xbox Series BT controllers at the same time previously collapsed them to a single Devices-page card with the live preview alternating between both physical inputs every couple of seconds.
Root cause was identity collision in BuildInstanceGuid's XInput-path branch. StableXInputInstance.Find returned the first non-HM HID instance for the given VID/PID, so both pads got the same physical path, the same MD5, and the same InstanceGuid. The polling loop then alternately overwrote that single Pads slot from each SDL device.
The XInput-path branch now retrieves every matching non-HM HID instance (sorted lexicographically for determinism across reboots / SetupDi enumeration changes), parses the slot index from SDL's XInput#N path, and picks the candidate at that index — appending :slot{slot} to the identifier when more than one candidate exists so two pads can never hash to the same GUID. Single-pad users see no change to their existing InstanceGuid.
--3.0.4--
Hotfix: PadForge v3.x.x failing to launch on non-English Windows (issue #69)
The v3.0.0 → v3.0.3 builds did not survive a relaunch on French / German / Japanese / etc. Windows installs. After a clean first run that successfully created a virtual controller, subsequent launches would either show the process briefly in Task Manager and exit, or sit running with no GUI.
Root cause was upstream in HIDMaestro (its issue #17, reported by @d3xMachina on Win 11 24H2 French). The HM SDK parsed pnputil.exe output by matching English-literal strings (Published Name, Driver package added successfully). Localized Windows installs print the same fields in the system locale, so PnputilHelper.IsHidMaestroDriverInstalled returned false even when the driver was already in the store. SetupController then ran FullDeploy per controller, the second deploy hit the localized already installed path the English greps couldn't recognize, and HM v1.2.0's throw-on-failure converted that into a hard crash before PadForge's UI could come up.
HIDMaestro 1.2.2 ships locale-stable parsing: pnputil /enum-drivers /format xml for enumeration (XML element names are pnputil's own identifiers, locale-stable), and a post-condition store check after /add-driver instead of grepping its rc/output. PadForge v3.0.4 picks up the new HM Core DLL.
If you hit issue #69 on v3.0.3, this build fixes it without any settings or registry cleanup on your end.
Other changes since v3.0.3
Touchpad click: first-class Touchpad 0 Click descriptor
The DualSense / DualShock 4 touchpad click is now mapped through a dedicated Touchpad 0 Click descriptor instead of being shoved into a numeric Button N slot. Sits parallel to the existing Touchpad 0 Finger M X/Y/Down descriptors that already covered the touchpad surface coordinates.
Auto-mapping picks it up out of the box on DualSense / DualShock 4.
MapToButtonPressedSinglerecognizes the prefix and routes tostate.TouchpadClick(single bool — Sony hardware ships one touchpad per controller, so theNis always 0).Mapping table shows live state for the descriptor.
Devices preview tints the touchpad surface border on press so you can see when the click registers.
Click-to-remap on a mapping cell now detects a touchpad press and writes
Touchpad 0 Clickdirectly. Previously did nothing because the recorder only scanned the numeric buttons array.
DualSense BT: touchpad click + HidHide
Two BT-side regressions cleaned up:
HidHide blocking now matches BT Classic DualSense paths. The validator was rejecting
VID&0002XXXXinterface paths because it required a literalVID_prefix; relaxed to accept theVID&form too.Touchpad click on the BT profile now actually fires descriptor button 14. The USB DualSense path drives byte 9 of Sony Report 0x01 directly through
SonyReportPackers, but the BT profile has no raw packer registered — so touchpad click silently dropped. Step 5 now ORsGamepad.TOUCHPADinto the gamepad-state mask for PlayStation slots when the touchpad is clicked, andHMaestroVirtualController.MapButtonstranslates that toHMButton.Touchpadso HM'sbuttonMaplands it on descriptor button 14 for both BT and USB DualSense / DS4 profiles.
DS5 USB packer: counter no longer bleeds into byte 9 button bits
Byte 9 of the DualSense USB input report (rgucButtonsAndHat[2]) is entirely button bits per SDL_hidapi_ps5.c: bit 0 PS, bit 1 Touchpad click, bit 2 Mute, bits 4-7 DualSense Edge function / paddle buttons. The packer was OR'ing (frameCounter >> 8) & 0x3F into the low six bits, so the PS and touchpad-click flags flickered as the counter rolled through the report at the polling rate (PS at ~4 Hz, buttons 14/15 cycling 00→10→01→11 at ~2 Hz). DS4 has its own counter packed into bits 2-7 of its byte 7; that does not apply to DS5.
Standardized button positions extended to 21 (Misc 1, paddles, Misc 2-6)
PadForge button positions 0-10 stay as the standard XInput layout (A/B/X/Y, LB/RB, Back/Start, LS/RS, Guide). Positions 11-20 are now wired to SDL3's extended gamepad button enum in Xbox Elite paddle order (P1=R-upper, P2=R-lower, P3=L-upper, P4=L-lower).
Mapping table shows the localized name for each (Misc 1, Right Paddle 1, etc.) across all nine languages. Touchpad click stays on its own Touchpad 0 Click descriptor — not a numeric Button slot — to keep parity with the existing touchpad coordinate descriptors.
Devices preview only shows buttons the device actually has
Every SDL3-recognized gamepad previously reported NumButtons = 21, so the Devices preview lit up 21 button circles for plain Xbox 360 / DualShock 4 controllers and the row summary always read 6 axes, 21 buttons, 1 POV(s). The extended slots only exist on Xbox Elite / DualSense Edge / etc.
ISdlInputDevice.SupportedButtonIndices now exposes the sparse list of button positions a device actually has — SDL_GamepadHasButton for positions 11-20, plus any raw-passthrough indices ≥ 21 not already consumed by the gamepad mapping. The preview circles, the per-frame IsPressed update, and the row's button-count column all read from this list. ForceRawJoystickMode bypasses the gate so you still see every native HID button when you flip that toggle.
HM 1.2.1 PXN VD6 profile
PXN VD6 wheel profile bundled with HM 1.2.1 (separate from the locale-stable parsing fix in 1.2.2 above).
--3.0.3--
Force Feedback tab on Keyboard+Mouse and MIDI slots (issue #54)
The Force Feedback tab is now shown for every VC type, not just Xbox / PlayStation / Extended. K+M and MIDI virtual controllers don't emit FFB upstream to games (no HID PID endpoint), but the tab is still meaningful: audio bass rumble feeds into the same combined-vibration buffer Step 2 sends to the physical mapped device, and ForceFeedbackState.SetDeviceForces applies the tab's overall gain, per-motor strength, and swap-motors scaling on its way out. Test Rumble fires the physical device's haptic surface directly, so it works regardless of VC type. The full tab makes sense as the layer audio rumble passes through on every slot type, not just the FFB-capable ones.
The Sticks tab still hides for MIDI; the Triggers tab still hides for K+M and MIDI — those map onto axes the K+M and MIDI mapping surfaces don't have.
Extended config bar: Rumble → Force Feedback
The "Rumble" checkbox in the Extended config bar is now labeled "Force Feedback" across all nine locales. The previous label undersold what the checkbox actually controls — the HID PID 1.0 force-feedback descriptor block, which covers constant, ramp, periodic (sine/square/triangle/sawtooth), and condition (spring/damper/friction/inertia) effects. "Rumble" is a strict subset.
The checkbox is now functional: toggling it actually drops or restores the PID descriptor block on the live device. Games that probe for PID FFB stop seeing the device's FFB capability when the checkbox is off. Useful for steering wheels whose own hardware drives FFB and you don't want games trying to drive both surfaces simultaneously. Customize-gated, same shape as the OEM Name Override and Product String overrides above it: the stored value persists across Customize toggle off/on so flipping Customize back on restores the user's setting; while Customize is off, the engine sees the catalog default regardless of any sticky non-default value.
Behavior table: Customizestored FFBresult offanycatalog profile as-is, PID block includedontruecatalog profile as-is, PID block includedonfalsecustom descriptor, PID block dropped
Settings persistence: three round-trip fixes
A codebase audit found three places where state was being silently dropped during settings save / profile switch / file load. All three are one-line patches in the persistence layer.
FFB toggle dropped from default profile on save. SettingsService.BuildAppSettings builds the AppSettings snapshot for the default profile inline, separate from the named-profile path. The inline initializer was missing ForceFeedbackEnabled = cfg.ForceFeedbackEnabled. Result: any user who set Customize=true, ForceFeedbackEnabled=false on the default profile would have FFB silently flipped back to true on the next save+reload, because ExtendedSlotConfigData.ForceFeedbackEnabled defaults to true on XML deserialization. Named profiles were unaffected — only the default profile took the broken path.
Touchpad overlay state dropped on profile switch. InputService.SnapshotCurrentProfile and the field-copy block in SaveActiveProfileState both omitted all seven EnableTouchpadOverlay / TouchpadOverlayOpacity / TouchpadOverlayMonitor / TouchpadOverlayLeft / Top / Width / Height fields, while ApplyProfile reads them. Symptom: switching profiles wiped overlay enable, position, opacity, and size to ProfileData defaults regardless of the live state. The on-disk XML round-trip via UpdateActiveProfileSnapshot was already correct; only the runtime profile-switch path was leaking the values.
UserSettings.Items.RemoveAll outside the SyncRoot lock. SettingsService.LoadFromFile purges orphaned MapTo<0 entries left by old PadForge.xml files. The purge ran outside the lock that the polling thread takes for FindByInstanceGuid / FindByPadIndex. Reload() from the UI menu can fire while the engine is running, putting List<T>.RemoveAll and a concurrent foreach on the same backing list — undefined behavior. Wrapped in the existing SyncRoot lock.
Notes on the audit
The audit also confirmed several categories are clean:
No bare locale-sensitive
.ToString()calls on doubles in the settings serialization path (issue #50 sweep is holding).All XAML
DataTrigger Value=strings use the v3 enum identifiers (Xbox/PlayStation/Extended); no leftoverMicrosoft/Sonyfrom the rename pass.ControllerModel{2D,3D}View/MidiPreviewView/KBMPreviewViewcorrectly pairBind()/Unbind()for bothCompositionTarget.RenderingandPropertyChangedsubscriptions; no event-handler leaks.DriverInstaller.UninstallViGEmBusandUninstallVJoyare kept on purpose for the v2→v3 migration wizard; not dead code.No live
// TODO/// HACK/// FIXMEmarkers across the codebase.
--3.0.2--
Profile switching: HM identity per slot
v3.0.1 carried two latent bugs in the runtime profile-switch path that together broke per-slot HIDMaestro identity tracking across high-level profiles.
HM profile slug stuck across profile switches. InputService.ApplyProfile re-set SlotCreated, SlotControllerTypes, Extended/MIDI configs, slot orders, and DSU/web/overlay state — but never touched the per-slot HM profile slug. Switching from a profile with xbox-one-s-bt to a profile saved with xbox-series-xs-bt left the slot stuck on xbox-one-s-bt. The save side compounded it: SnapshotCurrentProfile and SaveActiveProfileState both stripped SlotProfileIds out before storing, so each switch wiped the saved slug from the outgoing profile too. v3.0.2 wires both paths end to end. Step 5's per-slot diff at InputManager.Step5.VirtualDevices.cs:514-527 already destroys + recreates only the slots whose desired slug differs from the live HMaestroVirtualController.ProfileId — pointer-survival on slots that share a slug across profiles is automatic.
Spurious teardowns on profile switch.
Device-assignment apply used a reset-then-rebuild shape: every us.MapTo = -1 in one phase, reapply from profile.Entries in the next. The lock spans both phases but Step 5's IsSlotActive / HasAnyDeviceMapped reads don't take that lock. The polling thread could observe the reset window and fall into the immediate-destroy path at Step 5:590-600 — slots whose mapping is unchanged across profiles still got destroyed and recreated, including kernel-slot reallocation and the Xbox bubble-up cascade. v3.0.2 transitions each UserSetting directly old → new in a single pass, so identical-mapping slots ride through the switch with zero teardown.
One-time migration note. Profiles saved by v3.0.0 / v3.0.1 may have null SlotProfileIds arrays on disk. The first time you switch away from such a profile after upgrading, the new save path populates the array correctly. To force a populate on a specific profile: switch to it, set the HM slug you want, switch away. Round-trip once and the array is permanent.
Other
Wiki updated for the corrected ApplyProfile shape and SlotProfileIds round-trip.
Stragglers from the v2→v3 enum rename caught earlier this cycle (Xbox type-switch button highlight, Pads-page Xbox icon) ship in v3.0.1 — listed here for completeness.
--3.0.1--
The original v3.0.0 build wiped a virtual controller's slot configuration from PadForge.xml when its mapped devices stayed offline past the HM inactivity destroy timeout (default 60 s). Returning to PadForge after a long away-from-keyboard found the affected slots gone from the file. v3.0.0 has been pulled. v3.0.1 fixes the timeout to tear down the live HM controller only — the slot, mappings, profile, and per-group ordering are all preserved end-to-end, and the VC recreates automatically when devices return online. The sidebar power dot also stays green during the grace window now (it was flipping yellow the moment a device went offline). Two stragglers from the v2→v3 rename pass were caught in the same commit: an Xbox type-switch button highlight and an Xbox icon on the pad page that both still tested OutputType=="Microsoft" after the enum was renamed to Xbox. v3.0.0 release notes follow unchanged below.
Powered by HIDMaestro
Until v3, every Windows virtual-controller stack picked a side. ViGEmBus handled Xbox and DualShock 4 outputs. vJoy handled generic DirectInput sticks. Anything in between — a real Thrustmaster T300 with the actual T300 HID descriptor, an 8BitDo dance pad with the actual 8BitDo descriptor, a custom 8-axis 128-button rig — needed a third tool, or wasn't possible at all.
HIDMaestro takes both sides at once. 225+ device profiles, every one a real HID descriptor captured from real hardware. Xbox 360, Xbox One, Xbox Series, Elite, Adaptive. DualShock 3, DualShock 4, DualSense, DualSense Edge. Logitech wheels. Thrustmaster wheels and HOTAS rigs. Fanatec ClubSport. Hori fight sticks. 8BitDo gamepads. Dance pads. Racing wheels. Plus a synthetic Custom profile for fully custom HID descriptors with VID:PID, product string, and OEM name table override.
One driver. Every shape. User-mode UMDF2, installs without a reboot. The HIDMaestro SDK ships embedded inside PadForge.exe; the driver itself self-installs on first virtual-controller creation. Nothing to download, nothing to run manually.
This is the largest architectural change in PadForge's history. Every virtual-controller code path was rewritten on top of HIDMaestro's SDK. Two drivers became one. The "vJoy phantom controller" bug class is gone. The driver-install dialog and uninstall guards for the virtual-controller backend are gone — HIDMaestro is just there.
HIDMaestro is open source under MIT. Even if I close my IDE for the last time tomorrow, get hit by the proverbial bus, or otherwise meet an untimely demise, the community never has to fall back to ViGEmBus or vJoy again. HIDMaestro alone covers everything.
If you're upgrading from PadForge v2, the legacy driver cleanup dialog handles ViGEmBus and vJoy uninstall for you on first launch. Your PadForge.xml loads unchanged: the v2 on-disk enum strings (Microsoft, Sony, VJoy) round-trip to the v3 in-code identifiers (Xbox, PlayStation, Extended) via XmlEnum attributes.
One driver, every kind of controller
The Add Controller popup gives you five categories: Xbox, PlayStation, Extended, Keyboard+Mouse, MIDI. The first three route through HIDMaestro. Keyboard+Mouse uses Win32 SendInput and ships built in; MIDI uses Windows MIDI Services.
Per slot, a profile picker lets you choose any of HIDMaestro's 225+ profiles. The Xbox category surfaces Microsoft-vendor profiles. The PlayStation category surfaces Sony-vendor profiles. The Extended category gets everything else, plus the synthetic Custom profile for from-scratch HID descriptors.
Up to 16 simultaneous virtual controllers, mixed types. The XInput visibility cap of four is a Microsoft API limit (XInputGetState addresses user-indexes 0–3 only), not a backend ceiling. Slots beyond four still work; DirectInput, SDL, and raw HID see all sixteen. PlayStation, Extended, MIDI, and Keyboard+Mouse slots are unaffected by the cap entirely.
Three smaller features round out the engine:
HMOemNameOverride: Per-slot OEM name override surfaces through the Windows DirectInput OEM name table for games that key on the OEM friendly name.
HM inactivity destroy timeout: Idle HM virtuals destroy themselves after a configurable timeout (default 60 s, 0 = never), reclaiming kernel slots for active controllers.
Bubble-up cascade on slot deletion: When an HM slot is destroyed mid-stack, surviving Xbox HM VCs at higher pad indices shift down so xinputhid re-binds them to lower kernel slots, matching the natural disconnect/reconnect shape XInput does for real controllers.
Capture profiles from real devices
The Imported Profiles dialog in the Extended config bar reads the HID descriptor off any connected device. Plug in a controller, click Import, and the captured profile lands in the Extended dropdown with a (User Generated) suffix. Use it on every Extended slot going forward, export to JSON, share with anyone, or open a profile-contribution issue against HIDMaestro for the next release. No standalone tool, no extra downloads, no admin during capture.
Authentic PlayStation passthrough
PadForge is now the first remapper that passes a physical DualShock 4 or DualSense's full Sony Report 0x01 stream straight through to games. Gyro, accelerometer, touchpad fingers, and battery level all forward when the slot's HM profile is one of the DS4 or DualSense variants. Games that read the standard DualShock raw report shape see real motion, real touch, real battery — not a synthesized approximation.
The Sony Report 0x01 packer covers DS4 Type 1 and DualSense USB layouts. Profile-based dispatch by HM profile slug. The tools/Ds4InputDump/ utility ships a small raw-HID reader for verifying the byte layout frame by frame.
The touchpad surface itself can be driven by any source: a physical DS4 or DualSense touchpad, a Precision Touchpad, the on-screen touchpad overlay, or the web controller's touch zone. Routing happens through the Mappings tab.
Live touchpad on the virtual surface
The 3D and 2D PlayStation views render a live touchpad with finger-contact spheres (3D) and finger dots (2D). You see in real time which fingers are down, where on the surface, at what coordinates.
The touchpad surface itself is a click target. Click anywhere on the touchpad in either preview to record the TouchpadClick mapping — no need to find a button buried in a list. Map All on PlayStation outputs ends with TouchpadClick as the final recorded button. The 3D preview's touchpad dimensions are pinned to the real DS4 v2 touch surface (~52 × 23 mm).
Drag-reorder that just works
Slots within a type group can be drag-reordered on the Dashboard. Per-group order persists across restarts. The reorder model in v3 is repoint, not rebuild: kernel slots are anchored to visual position; data identities (mappings, devices, profile selection) are repointed to the kernel VC at their new visual position.
For same-profile reorders, this is a pure pointer swap in _virtualControllers[] plus a FeedbackPadIndex update on each moved VC. Zero kernel teardown. Zero game-side disconnection. Reordering three Xbox 360 slots takes effectively no engine time.
For different-profile reorders, only the specific positions whose profile actually changed get destroy + recreate. Matching positions in the same reorder still pointer-swap. Cross-group moves (changing a slot's type) go through type-change detection in Pass 1 and are handled separately.
HID PID 1.0 force feedback
Extended profiles that include the FFB descriptor block decode the full HID PID 1.0 effect set. Constant. Ramp. Periodic (sine, square, triangle, sawtooth). Condition (spring, damper, friction, inertia). All routed to the physical wheel or joystick driving the slot, with directional pass-through.
HMaestroFfbDecoder does the decode; HMaestroFfbDescriptor appends the PID descriptor block to Extended profiles that opt in. The condition-effect reader is now bit-aware, fixing several incorrect "approximate" comments on byte-aligned reads that were silently truncating values.
On-screen touchpad overlay
The on-screen touchpad overlay accumulated a long list of small bugs in v2. v3 cleared them:
Drift-proof touch counter, reset on visibility change.
px↔DIP conversion at the Win32 boundary so drag and resize deltas scale cleanly with display DPI.
Self-heal off-screen saves: if the saved position is outside the current monitor configuration, it relocates back into a visible area on next launch.
Reset Position button restores the overlay to a default visible spot.
Press-and-hold-to-right-click gesture suppression.
Title bar's WPF touch promotion suppressed so double-clicks fire on the title bar.
Surface alpha floored at 1 so low-opacity overlays stay touch-a..
--2.4.1--
Add Controller Popup
Theme-aware — background and icons follow dark/light theme
Live theme updates — popup background updates instantly when theme changes while open
Dismiss on outside click — clicking anywhere outside the popup closes it
Cleaner shadow — separate shadow element pattern eliminates corner artifacts
Touchpad Overlay
Opacity 0% — slider and number field now allow fully transparent
Reset buttons added next to DSU port, Web port, and Opacity fields
Wider port fields (140 → 160px) so 5-character port numbers don't get clipped
Updated description — "On-screen touch surface for DualShock 4 and DualSense-compatible touchpad emulation" (10 languages)
DSU Motion Server
Updated terminology — "cemuhook" → "CemuHook Motion Provider protocol" (10 languages)
Code Quality
WebControllerDevice.RawButtonCount now respects touchpad-only device flag
Removed redundant null check in DevicesViewModel
Removed redundant empty-string check in InputManager.Step3.ParseDescriptor
--2.4.0--
Profile Shortcuts
Switch profiles via controller button combos — assign Next, Previous, Specific, or Toggle Window actions to any button/axis combination
Cross-device combos — combine buttons from different controllers (e.g., Guide on gamepad + pedal on flight stick)
Axis direction triggers — use stick/trigger directions as shortcut inputs (e.g., Guide + LX left = Previous, Guide + LX right = Next)
5-second recording with live preview and countdown — press Record, hold your combo, auto-commits on timeout
Toggle Window shortcut — minimize/restore PadForge from a controller without touching the keyboard; forces to foreground even over fullscreen apps
Device filtering — trigger from Any Device or a specific controller
Profile Switch Flyout
Win11-style notification above the taskbar when profiles switch — pixel-matched to the native volume OSD
Four-stage flow: Profile Name → Initializing (flashing icon) → Active (accent checkmark) → auto-dismiss
Offline warning: "One or more controllers offline" with amber icon when devices are missing
Slide animation — slides up from the taskbar edge and down on dismiss
Theme-aware — follows system dark/light theme
DS4 Touchpad Support
Multi-source touchpad input — SDL3 gamepad touchpad, Precision Touchpad (PTP), touchscreen overlay, and web controller all feed into DS4 virtual controller via DS4_REPORT_EX
Touchscreen overlay — on-screen touchpad surface for touch devices, draggable and resizable
Web controller touchpad — touch zone on the web controller page for remote touchpad input
Devices page touchpad preview — live finger position and contact visualization
Precision Touchpad reader — captures PTP trackpad input via WM_INPUT for touchpad-to-DS4 passthrough
WPF UI Migration
ModernWPF 0.9.6 → WPF UI 4.2.0 — full migration to actively maintained Fluent Design framework
App branding bar with custom fullscreen button and native Win32 tooltip
Compact sidebar — 48px items, 15px font, Windows 11 design language
Dashboard card polish — drag stability at high DPI, power button hover, visual parity with sidebar
SDL GUID in Mapping Submission
Devices page shows SDL GUID — needed for gamecontrollerdb mapping submissions (VID/PID alone can't build the CRC-based GUID)
Submission workflow includes SDL GUID in the generated issue template
Force Feedback Fixes
Fix directional data loss — Step2 was dropping FFB direction data, routing all effects to scalar rumble
Motor split direction convention — polar direction correctly maps to left/right motor split
FfbTest tool — standalone build, .NET 10, interactive polar direction control
Map All Toggle
Single toggle button — "Map All" switches to "Stop" while active, replacing the separate stop button
Both tabs — works on Preview and Mappings tabs
Bug Fixes
Fix fullscreen state not restoring from tray or minimize
Fix fullscreen button foreground stuck on light theme when starting minimized to tray
Fix overlay crash on shutdown — stop timers before closing window
Fix KBM mappings missing from HasAnyMapping, ClearMappingDescriptors, GetAllMappingDescriptors (KBM-only slots excluded from Copy From dialog)
Fix window position/size/maximized state not persisting across restarts
Fix DS4 touchpad macro ignored (#51)
Fix mouse handle mismatch after PTP registration
Fix 2D controller view crash — pack URI scheme failure
Fix device identity confusion in same XInput slot
Fix WPF GPU device loss on sleep/wake
Spelling & Localization
Standardize "deadzone" — updated from "dead zone" to match Microsoft's official gaming terminology across all code, UI strings, documentation, and wiki (10 languages)
Rename "Awaiting Controllers" → "Awaiting Devices" — technically accurate since assigned inputs may not be controllers
Code Quality
Remove duplicate FormatExePaths method
Fix misplaced XML doc on SwapSlots/MoveSlot
Rename QueueProfileSwitch → HandleGlobalMacroAction
Remove unused DashboardViewModel.ViGEmVersion
Clean up stale fields and missing null cleanup
...More to be added