DSZ - DualSense Zen is a fast, native Windows companion for the PlayStation DualSense, DualSense Edge, and DualShock 4 controllers, written in Rust: lighting, adaptive triggers, Bluetooth haptics and speaker audio, gyro and touchpad mouse, button mapping, virtual Xbox 360 / DualSense / DualShock 4 controllers, and per-game profiles.
- Game haptics and adaptive triggers over Bluetooth. Games with native DualSense support normally only do this over a USB cable.
- Automatic DualSense mode. When you launch a game, DSZ detects whether it supports the DualSense natively. If it does, DSZ switches to a virtual DualSense, so the game's own haptics and adaptive triggers work. For a Steam, GOG, or Xbox app game that doesn't, you choose the virtual controller: DualShock 4, Xbox 360, or the profile's own. It switches back when the game closes. (How it works)
- The rest of the controller: lighting, adaptive triggers in any game, system audio as haptics, touchpad and gyro mouse, button mapping, and per-game profiles.
- Extremely fast and lightweight: a single
.exewith nothing to install. It starts in about 0.2 s and uses under 1% CPU in the tray. (Performance) - Free and open source, with no account or license check.
- Windows 10 or 11 only, for the DualSense, DualSense Edge, and DualShock 4 (an official pad, Sony's USB adapter, or a common pad that speaks the DualShock 4 reports). A DualShock 4 gets buttons, sticks, gyro, the touchpad, the lightbar, and classic rumble, plus experimental speaker and headphone sound over Bluetooth (off by default). Adaptive triggers and the Bluetooth haptic stream are DualSense only.
- Virtual controllers need two free, open-source drivers: usbip-win2 (the app installs 0.9.8.1, the current release; an already-installed 0.9.7.7 or 0.9.8.0 attaches too) and HidHide. The app installs either in one click (see Drivers). Without them it still does lighting, triggers, haptics from system audio, mouse modes, and mappings, but games see the real controller.
- Close other controller apps while it runs. Only one app can drive the controller at a time. If buttons double up or lights flicker in a Steam game, turn Steam Input off for that game.
- This is an early release (0.3), tested on one PC with a DualSense Edge over Bluetooth. DualShock 4 support has not been tried on a real DualShock 4 yet. Expect rough edges, and please report them.
Measured on one PC with a DualSense Edge over Bluetooth (about 670 input reports a second), launched cold with no game running, next to another popular DualSense app with the same setup. Figures are rounded; results on other PCs will differ.
| DSZ | Another DualSense app | |
|---|---|---|
| Startup | ||
| Window visible after launch | about 0.2 s | a few seconds |
| Virtual controller ready | about 0.2 s | several seconds |
| Input lag (controller to virtual controller) | ||
| Typical | a few hundredths of a ms | about the same |
| Worst case | under 1 ms | occasional hitches of tens of ms |
| Game trigger feedback (game sends an effect, until the trigger motor moves) | ||
| Typical | about 30 ms | about 40 ms |
| Window open, controller in use | ||
| CPU (% of one core) | about 7% | about 3× more |
| Memory | about 100 MB | about 4× more |
| In the tray, game rumble playing | ||
| CPU (% of one core) | under 1% | about 2× more |
| Memory | about 100 MB | about 4× more |
Trigger feedback includes both Bluetooth trips and the trigger motor starting, so most of it is the controller itself.
Part of the low overhead is that every virtual controller is served inside the app itself, built from the USB/IP and USB Audio Class specifications. No separate USB/IP server such as VIIPER is used, so there is no extra process and no IPC hop. And because the app contains no VIIPER code (VIIPER is GPL-3.0), it stays MIT.
Download dsz.exe from the Releases page and run it; there is nothing to install. The exe isn't code-signed, so the first time you run it Windows SmartScreen may say "Windows protected your PC": choose More info, then Run anyway.
DSZ asks GitHub once a day whether a newer release is out (Settings → Updates, where the check can be turned off). It says so in the window, in the sidebar, and with a tray notification, but installs nothing by itself, since a restart reconnects the virtual controllers. DSZ never downloads a program itself (an app that fetches an exe and starts it looks like a "downloader" to antivirus heuristics, which is why Kaspersky flagged 0.3.8). Download has your browser download the release's dsz.exe, so it is scanned like any download. DSZ finds it in the Downloads folder (or I saved it somewhere else… points to it), checks it against the SHA-256 GitHub publishes for it, and offers Install and restart: that renames the running exe to dsz.exe.old, copies the new one into its place, and starts it; the new version deletes the old file. If DSZ can't write to its folder, it says so; replace dsz.exe there by hand.
Settings, profiles, and the log live in %APPDATA%\DSZ\, unless a folder named dsz-data sits next to dsz.exe: then they live there, and nothing goes in %APPDATA%. Settings → Backup & restore → Keep settings beside dsz.exe makes that folder and moves everything into it (or back), then restarts DSZ; making an empty dsz-data folder by hand also works, starting from fresh settings. Start with Windows is a registry entry and is not carried by the folder.
On Windows with the Rust toolchain (MSVC):
cargo run --release -p dszThe release build is a single file, target\release\dsz.exe. Settings, profiles, and the log live in %APPDATA%\DSZ\ (or dsz-data beside the exe, see Portable). Settings → Backup & restore saves every profile and setting to one file and restores it (the current setup is saved to backups\ first); "Open settings folder" there opens the folder in use.
Flags: --minimized starts in the tray (the window isn't shown until the tray opens it); --page <name> opens a page (lighting, adaptive, haptics, motion (or input), button, virtual, profiles, settings); --edit <profile> opens a profile for editing without activating it; --probe-virtual <hidhide|usbip-x360|usbip-ds4|usbip-ds|usbip-mic|install-check|game-output|tone|default-audio|speaker|speaker-ab|mic-listen> checks one virtual-device driver from a console (usbip-x360 attaches a virtual Xbox 360 pad and checks XInput and rumble; usbip-ds4 reads a virtual DualShock 4 back through HID; usbip-ds attaches a virtual DualSense, reads it back through HID, and plays a 4-channel tone into it; usbip-mic attaches the microphone device used beside an Xbox 360 or DualShock 4 pad and sends a 440 Hz tone to whatever records from it; install-check <file|url> shows what the driver-install check sees; game-output and tone act like a game against the running app's virtual DualSense). DSZ_PERF=1 logs UI frame rate. DSZ_DATA_DIR=<folder> runs with a separate settings folder, for testing. In debug builds, DSZ_SNAPSHOT=<file.bmp> renders the window once, saves it, and exits (it doesn't open controllers, so it runs beside the real app), and DSZ_SNAPSHOT_SIZE=<W>x<H> sets the window size in points; DSZ_DEMO=1 shows the no-controller drawing with a few inputs live.
A fresh install starts with two profiles, from crates\dsz\defaults\: Xbox (a virtual Xbox 360 controller, touchpad as mouse, a dim red lightbar) and DualSense (a virtual DualSense with game haptics, PS works as the Xbox button: hold it for Task View). A long press on Mute switches between them, and games with native DualSense support switch to DualSense on their own.
| Area | Features |
|---|---|
| Connection | Bluetooth and USB; DualSense, Edge, and DualShock 4 (official pads, Sony's USB adapter, and common DS4-shaped gamepads), hot-plug in under a second. Read timeouts never drop the device and write errors back off and retry; a controller goes away only when Windows removes its node. Bluetooth input CRCs are checked, and a DualShock 4 clone whose CRC never matches is still read |
| Lighting | Static, rainbow, breathing, strobe, battery gradient, off, and system-audio loudness (green, yellow, red, with thresholds); brightness and speed; player LEDs (player 1–5, custom, battery gauge); mute LED (off, on, pulse, toggle with button); low-battery warning (a double blink every 2.5 s, 15 % by default) |
| Adaptive triggers | Resistance, weapon, vibration, slope, per-zone resistance and vibration (drag to paint), bow, galloping, machine, and named presets (Very soft to Rigid, GameCube click, Choppy, Vibrate pulse) built from those effects; live zone preview with the current pull |
| Wireless haptics | PCM haptics over Bluetooth (report 0x32, one every 32/3000 s), from system audio (WASAPI loopback, any output device) with high-pass and low-pass filtering, stereo, and gain; button clicks placed under the grip you pressed; trigger texture. The stream takes the actuators only while something needs it (system audio, clicks, trigger texture, game haptics, a test pulse); the rest of the time rumble goes to the controller's own rumble emulation (power and "improved rumble" apply). While streaming, or with "Rumble as haptics" on, rumble plays through a rumble-to-haptics conversion: heavy motor 34–50 Hz on the left, light motor 85–110 Hz on the right, weak rumble lifted, smoothed. The 0x31 control report keeps flowing alongside, and changes from a game or mod are sent at once instead of on the next 8 ms tick. Game actuator channels from a virtual DualSense mix into the same stream (see Virtual controller) |
| Controller speaker | Speaker sound over Bluetooth (report 0x39): a game's own speaker channels from a virtual DualSense, and optionally PC audio, which is separate from the haptics source: PC sound can play on the speaker or headset while the haptics come from the game, or also drive the haptics ("Audio to vibration"). While there is speaker sound the stream sends a report every 21.3 ms (two haptic blocks at 3 kHz) with two Opus frames (libopus, built in: 160 kbit/s CBR). The controller plays both frames over that one tick, as its own microphone shows (a 440 Hz tone sent as plain 48 kHz comes back at 412.5 Hz), so each report carries 21.3 ms of audio squeezed 16:15 with a windowed-sinc resampler. A stream starts with three silent reports as a cushion against radio jitter, and returns to 0x32 five seconds after the sound stops. --probe-virtual speaker and speaker-ab check all of this on a controller without injecting input. Some Bluetooth adapters pause the link for about 25 ms a couple of times a second (seen as late input reports even with nothing streaming); each pause can leave a short gap in the speaker sound, in any app. --probe-virtual mic-listen --full measures those pauses without writing to the controller. The speaker plays a mono mix; with "Headset when plugged in" a headset in the controller's jack gets stereo instead (Opus packet 0x16, the headphone volume set from the speaker volume, the speaker muted), switching both ways as the plug goes in and out. The game's clock is followed with a small read-rate correction instead of dropped frames, and sound fades in and out at gaps, so the speaker doesn't click. Volume, a level meter and a test sound are on the Haptics page |
| DualShock 4 speaker | Experimental and off by default (Haptics page, per profile; Sony DualShock 4 v1 and v2 over Bluetooth only). PC audio, a game's speaker channels from the virtual DualSense (a DualShock 4 can drive a virtual DualSense; this needs the virtual output's Game speaker on too, and plays only while a game has its audio endpoint open), or both mixed, to the controller's speaker (a mono mix) or its headphone jack (stereo), with a volume. The way the audio is sent comes from DS4 Sound Link by abh1shekbana (u/Livid-Ad-2313 on Reddit): the 0x11 control report arms the audio path (byte 2 0xA0, which keeps input reports flowing) and sets the headphone and speaker volume, then a 0x17 report every 16 ms carries four 109-byte SBC frames (32 kHz, 16 blocks, joint stereo, SNR, 8 subbands, bitpool 48) to the speaker (0x02) or the jack (0x24). Its SBC encoder is ported to Rust (ds-proto/src/sbc.rs) and produces the same bytes, which the tests check. Sound is low-passed and resampled to 32 kHz, with about 48 ms of PC audio buffered. While a source is on, the stream runs all the time (silence between sounds keeps the amplifier from popping), which uses more battery. Turning it off sends one 0x11 with the audio path off and the volumes at 0, and then the reports go back to the plain ones. Until a source is turned on, every report is byte for byte what it was. Over USB the controller is already a Windows sound device. Not yet tried on a real DualShock 4 |
| Bluetooth microphone | Experimental and off by default (Haptics page, per profile). The controller's built-in microphone, or a headset microphone in its jack, becomes a Windows recording device ("Wireless Controller"), so Discord or a game can use it over Bluetooth. With a virtual DualSense it is that pad's own microphone; with a virtual Xbox 360 or DualShock 4, which have none, the app plugs in a second USB device beside the pad that is only the DualSense's microphone (its audio control and capture interfaces, no game pad, so no game sees an extra controller and the Xbox 360 pad's descriptors are untouched). That device is there only while the microphone is on in the profile and the pad is in use, and goes away with the controller. Source (auto, built-in, headset), mute, gain, the Mute button as a real mute (its LED shows the state), and a level meter. The controller's microphone runs only while an app records from it (and Windows hasn't muted it), so the rest of the time the radio and idle power-off are as before. Packets (0x31 type 2) are checked by CRC and decoded on their own thread (Opus, with loss concealment for short gaps), through bounded queues, and speech left in a buffer is dropped on mute, source change and disconnect. Windows' microphone level starts at the controller's own level; lower is quieter. Until the microphone has been turned on for a connection, the speaker and haptics reports are byte for byte what they were before; after that their microphone bit follows recording. Needs Bluetooth and a virtual DualSense, Xbox 360 or DualShock 4 controller; over USB, use the controller's own Windows microphone. --probe-virtual usbip-mic plugs in the microphone device alone with a test tone |
| Motion & touch | Gyro mouse (always, hold, unless held, toggle, or while a finger is on a touchpad third, with no click; turn, lean, or both; soft deadzone, tiered smoothing, acceleration, auto and manual calibration), touchpad mouse (one-finger tap left click, two-finger tap right click, press to click and drag, optional right-half right click, two-finger scroll; movement waits 60 ms after a finger lands so taps and presses don't nudge the cursor), sticks as mouse, scroll, WASD, or arrows. Directional reach, off by default, compensates weak directions independently for each stick: select Game only and a virtual controller in the profile, then enable it under Sticks. Its 16 directions start at up and run clockwise, with smoothly blended gains from 50–150%, draggable points and fine adjustment. Calibrate reach needs at least three complete turns in either direction, holding the stick against the rim; review the preview and choose Apply or Discard. It estimates the rim at each direction to avoid shrinking a direction because a nearby angle reached farther; missing directions are marked and keep their gains. Calibration aims for about 100% at the rim within the gain limits; intermediate movement stays proportional. Raw/corrected percentages show vector magnitude before the deadzone or curve, so diagonals may exceed 100%. Reset all restores 100% gains and clears the measured trace. Gains are saved per profile; measurements are temporary. Old profiles keep their behavior |
| Mappings | Any button or button combo, including Edge paddles and Fn, to keys and key combos, mouse buttons, scroll, media keys, a PlayStation or Xbox controller button, switching to a named profile or next/previous, using a profile only while a button is held, toggling gyro or touch mouse, launching a program, or turning a Bluetooth controller off. Touchpad Left, Center, and Right are the touchpad click in each third of the pad, next to the whole-pad Touchpad. Press, long press, double tap; hold, tap, turbo, toggle. "Hide from game" removes the button (or the combo's buttons while it is held) from the virtual controller. Use profile while held is a chord or shift layer, like Steam Input's chords: the controller uses another profile while the button or combo is down and goes back as soon as one of its buttons comes up, or, when on release names a profile, switches to that one and stays there. It works with press, long press, and double tap; the hold, tap, turbo, and toggle setting doesn't apply. The held button doesn't trigger its own mappings in the held profile, so only the other buttons pressed meanwhile use the held profile's mappings. "Hide from game", ticked when you choose this action, keeps the held buttons from the game for the whole hold, and each one until it is let go. The virtual controller stays the one of the profile underneath, so the game's pad is never replugged; mappings, mouse modes, lighting, and triggers come from the held profile. A hold is never saved. If a game starts or closes during it, the release goes back to that game's profile. A profile picked during it (in the app, the tray, or by another mapping) stays after the release. If the controller disconnects, it comes back on the profile from before, and only one hold runs at a time. An Xbox button on a profile without a virtual Xbox controller goes to a helper Xbox 360 pad, because Windows takes the Xbox button only from an Xbox controller (PS on a virtual DualSense or DualShock 4 does nothing). With HidHide the helper is hidden before Windows sees it, from everything but explorer.exe (which opens Task View on a Guide hold) and Game Bar, and stays plugged in while a profile uses it; the nodes Windows makes below it are hidden as they appear and remembered for next time. Without HidHide the helper is plugged in for the press (games can see it; Windows needs about a second to notice it, so the first press is replayed late) and unplugged 30 seconds later |
| Virtual controller | Per profile: off (native), Xbox 360, DualSense, or DualShock 4. Every virtual pad is a wired USB controller the app serves itself over USB/IP to usbip-win2, so that is the only virtual-controller driver needed. The Xbox 360 pad presents the real controller's USB identity and vendor interface, so Windows' built-in Xbox 360 driver (xusb22) loads and XInput sees it; rumble and the ring LED (which gives its XInput slot) come back on its OUT endpoint. SDL games find it through raw input, which lists a device only at the moment it arrives, so a pad plugged in for later is hidden only after raw input has listed it, and the pad in use is checked every 5 seconds and plugged in again if raw input has lost it. A pad hidden while parked is plugged in again when it comes back into use only if something may have looked for controllers while it was hidden (it was hidden when Windows first saw it, or a game started meanwhile is still running), since showing a device again makes no arrival; otherwise the switch back makes no reconnect. Game Bar no longer reads DualSense pads (Windows updates of October 2026), so while its overlay is open (found by hit-testing the screen, since it never takes the foreground) the parked pad gets the whole mapped pad and the game's virtual pad sends idle input; Game Bar is let through the parked pad's hiding. The DualShock 4 is a plain HID device with the controller's report layout, carrying the DualSense's motion and touch, for games that show PlayStation prompts only for one; its rumble and lightbar come back. The virtual pad is plugged in when the app starts, before any controller, so it takes the first free XInput slot, and it stays connected (idle, with the real controller still hidden) while the controller sleeps or drops out; the controller takes the same pad back when it reconnects. "Stay connected while the controller is off" on the Virtual controller page turns this off. Games get the mapped and muted pad with per-stick deadzones (radial, axial, or a curve with inner and outer zones, a response curve, and an optional lift), axis inversion, and trigger ranges. Game rumble comes back and plays as haptics over Bluetooth. The physical controller is hidden with HidHide while the virtual one is up, this app is whitelisted, every change is undone on exit, and crash leftovers are undone at the next start. DualSense mode is a wired DualSense the app serves itself over USB/IP to usbip-win2: games get its HID interface and its 4-channel 48 kHz audio endpoint, and the app relays output reports (rumble, adaptive triggers, LEDs) and game audio (actuators to haptics over Bluetooth, or classic rumble when PCM is off; speaker channels to the controller speaker over Bluetooth). Windows' habit of making a new USB audio device the default is undone, so system sound and the default microphone stay where they were; if you pick the virtual DualSense as the default yourself, that choice is kept, also the next time it is plugged in. The same goes for the microphone device used beside an Xbox 360 or DualShock 4 pad. See below |
| Profiles | Create, duplicate, rename, delete, import and export single profiles as JSON (back up or restore everything at once in Settings); per-controller assignment, or every controller on the default profile at startup; per-game automatic switching (most recently started game wins, restored on exit). Games with native DualSense support switch to a chosen DualSense profile without a rule: each new program's folder and exe are checked once for Sony's pad library (libScePad, scePad* calls), Wwise's DualSense haptics sink (AkScePad), DualSenseWindows (ds5w), DualSense controller names, a DirectInput button mapping for the DualSense or Edge (0CE6054C-0000-0000-0000…), a UTF-16 "DualSense" together with hid.dll (a game with its own HID code, or Unreal's WinDualShock plugin), SDL with its PS5 driver (SDL_JOYSTICK_HIDAPI_PS5, built into the exe, or in an SDL2.dll/SDL3.dll beside it that the exe opens gamepads through), or Rewired's DualSense driver (DualSenseExtension in <game>_Data\Managed\Rewired_Core.dll), and the answer is cached in dualsense-detect.json. A cached "no" from an older version of the check is checked again. On the reference PC this flags Alan Wake 2, Baldur's Gate 3, Elden Ring, Final Fantasy XIV, Animal Well, Oddsparks and RPCS3, and not Creed: Rise to Glory, Chrome, Edge, Discord, VS Code, the Epic launcher, Rocket League, Octopath Traveler II, or any of 512 other programs installed there. --probe-virtual dsdetect [--path <exe>] runs the check by hand |
| Mod API | UDP server on 127.0.0.1:6969 speaking the protocol existing DualSense game mods use (see Mod compatibility): TriggerUpdate with trigger modes 0–26 (custom trigger values Off through PulseAB), RGBUpdate, PlayerLED, PlayerLEDNewRevision, TriggerThreshold, MicLED, ResetToUserSettings, and GetDSXStatus, with the status reply mods read. Extensions: HapticPulse [index, left%, right%, Hz, ms] and Rumble [index, heavy, light, ms] |
| Emulator motion | Off by default. Settings → Emulator motion turns on a DSU (cemuhook protocol) server on 127.0.0.1:26760, which gives Dolphin (Wii Remote motion and pointing), Cemu (Wii U GamePad), and Ryujinx and yuzu-family Switch emulators each controller's gyro and accelerometer, plus its own buttons, sticks, touch, and battery, in slots 0–3 by connection order. Motion is sent the way DSU clients expect from a DualShock 4 (accelerometer in g, gyro in deg/s with DSZ's bias correction). Dolphin doesn't map the motion inputs itself on Windows: each Accelerometer and Gyroscope box in the Wii Remote's Motion Input tab needs its DSU input, such as `DSUClient/0/DSZ:Gyro Pitch Up` for a server named DSZ, and the card says how. The profile's gyro mouse should be off while playing |
| Battery | The haptic stream runs only while something needs it. Animated lighting sends at most 40 control reports a second (34/s measured on an Edge, down from 100), while trigger, rumble, and LED changes still go out at once. Idle power-off for Bluetooth is in Settings. The output report's power-save bits were tested and do not switch the motion sensors off over Bluetooth, so they stay unused |
| App | Tray icon with profile switching and battery tooltip, close-to-tray, start with Windows, Bluetooth power-off and idle shutoff, live log viewer. The interface is in English, Português (Brasil), or Español: choose Settings → Language → Español for Spanish, applied immediately and saved automatically; English is the default. Translations cover pages, tooltips, notifications and dialogs. They live in crates\dsz\locales\pt-BR.json and crates\dsz\locales\es.json, keyed by the English text, and a missing entry shows English. Logs, CLI arguments, profile names and saved identifiers keep their original text |
The first time a game runs, the app checks its files once for signs of native DualSense support: Sony's PC pad library, Wwise's DualSense haptics, DualSenseWindows, DualSense controller names, a DirectInput mapping for the DualSense (as in Final Fantasy XIV), its own HID code that names the DualSense (as in Animal Well), SDL with its PS5 driver (as in Astral Ascent and Blood: Refreshed Supply), or Rewired with its DualSense driver (as in Death Squared). SDL and Rewired games read the DualSense natively, so they show PlayStation prompts and use rumble and the lights, though most don't use the haptics or adaptive triggers. While a game that has them is running, the controller switches to your DualSense profile, which presents a virtual DualSense, so the game's own haptics and trigger effects reach the controller. When the game closes, it switches back.
Steam, GOG, and Xbox app games without that support can present a virtual DualShock 4 or Xbox 360 instead (Profiles & games, Other games). Until you choose one, they keep the profile's own virtual controller. Store apps that aren't games and often run in the background (Wallpaper Engine, Lossless Scaling, SteamVR, redistributable installers) don't count. A per-game rule overrides the check, and a long press on Mute switches by hand.
Game mods that drive adaptive triggers and lights send JSON over UDP to port 6969. DSZ's side of that protocol follows what these public mods send and read: instruction types, trigger modes and the order of their parameters, LED values, and the status reply. Trigger modes 19–26 follow Nielk1's trigger effect generator (MIT), whose parameter order the mods that use them match. The named presets (Very soft to Rigid, GameCube, Choppy, Vibrate pulse) are DSZ's own effects, since mods send only the name. Custom trigger values support Off, Rigid, and Pulse (plain, A, B, and A+B), with the mode bytes pydualsense documents.
udp.rs has one test per mod that replays packets as that mod builds them (number or string parameters, booleans, missing parameter lists) and checks what DSZ does with each.
| Mod | Game | License |
|---|---|---|
| dvize/TarkovDSX | Escape from Tarkov | MIT |
| dvize/DSXFallout4 | Fallout 4 | MIT |
| dvize/DSXSkyrim-NG | Skyrim | GPL-3.0 |
| tommargar/DualSense4Rockstar | GTA V, Red Dead Redemption 2 | none stated |
| SYSTEMATI0N/WF2_DSX | Wreckfest 2 | MIT |
| cosmii02/ForzaDSXlegacy | Forza Horizon 4 and 5, Forza Motorsport 7 | none stated |
| aaronfang/RBR_Adaptive_Trigger | Richard Burns Rally, Assetto Corsa | none stated |
| RiddleTime/Race-Element | Sim racing (ACC and others) | GPL-3.0 |
| tpetsas/DSXpp | C++ client library for mods | MIT |
| Tio-Henry/DualSenseX-Support | Godot engine add-on | MIT |
Nothing is bundled. The Virtual controller page shows what is installed and installs a missing driver in one click: it downloads the pinned release from the project's GitHub page, checks it (usbip-win2 against its published SHA-256, HidHide by its author's Authenticode signature, since that release publishes no hash), and opens its installer, which asks for admin. install.rs has the URLs.
| Driver | Used for | On the reference PC |
|---|---|---|
| usbip-win2 0.9.8.1 (free, BSD-2, Microsoft attestation-signed) | Every virtual controller: Xbox 360, DualShock 4, and DualSense with game haptics | Works: Xbox 360, the hidden helper pad, and the virtual DualSense with its game audio endpoint. On 0.9.7.7, the release the timings were taken on: Xbox 360 in XInput slot 0 within 0.19 s with rumble and LED back; DualShock 4 read back through HID; DualSense input at the physical report rate, with output reports and the 4-channel audio endpoint at 48 kHz. Not 0.9.7.8, which winget installs by default: its author warns it corrupts memory, it blue-screened the reference PC twice, and the app refuses it. 0.9.7.7 and 0.9.8.0 attach too, if they are already installed: each release changed the attach request (0.9.8.0 made it larger, and on 0.9.8.1 the bus id, service, and host sit four bytes later), which is why the app pins a release it knows rather than the newest one. Upgrading from 0.9.7.7 took two restarts on the reference PC, and the first try hung in 0.9.7.7's uninstaller until a restart |
| HidHide 1.5.230 (free, open source, signed) | Hiding the physical controller from games while a virtual one is up | Works |
The virtual DualSense is the app's own code (virt\usbip\): a USB/IP server on 127.0.0.1 that answers every URB, attached through usbip-win2's PLUGIN_HARDWARE_ONCE IOCTL. Isochronous audio URBs complete at the end of their real-time window, so the Windows audio engine runs at 48 kHz, and PCM reaches the haptics streamer as soon as a URB arrives. Its USB serial number (served in its own device descriptor) starts with DSZV, which is how the app skips its own virtual pad when it looks for physical controllers. The virtual DualShock 4's serial is 12 hex digits instead, because Steam only treats a pad as a DualShock 4 when it is: 0244535A (a locally administered address, then "DSZ") and the last two bytes of the physical controller's address, so Steam keeps each controller's name, lightbar color, and settings apart (the pad slot when the address is unknown, or when another virtual DualShock 4 already has that serial). A DualShock 4 driving a virtual DualSense gives it that same address for pairing, for the same reason. A DualShock 4 that another app makes in software (DS4Windows through ViGEm, for example) is skipped too: its device hangs straight off a root-enumerated bus, not a hub or the Bluetooth stack. When a game asks it for calibration, pairing, or firmware information, it answers with the physical controller's own reports, read when that controller connected (neutral calibration and a DSZ address before any has); other feature requests are declined. Its HID report descriptor is the one dumped from a real DualSense in dogtopus's write-up. The installed release is read from usbip.exe's version, since the driver files have none, and 0.9.7.8 is refused, also by its driver's July 2026 build date, so an older usbip.exe left next to it doesn't let it through. 0.9.8.0 and 0.9.8.1 are not refused: attach tries each known request size, and the serial field of the newer requests is left empty so the descriptor's own serial stays.
No user-mode API creates a virtual audio endpoint, and InputInjector gamepads are invisible to XInput, so neither feature is fully driverless.
build.rs embeds a VERSIONINFO resource (FileDescription and ProductName "DSZ - DualSense Zen", version from Cargo.toml) and the app icon, drawn by src\icon_art.rs and written as a multi-size .ico at build time. Task Manager, Startup apps, the taskbar's tray-icon settings, notifications, and Explorer show "DSZ" and the icon instead of dsz.exe. It uses the Windows SDK's rc.exe through the winresource build dependency. Without the SDK the build still succeeds, with a warning and no version info.
| Path | What |
|---|---|
crates\ds-proto |
Pure protocol: input parse, 0x31/0x02 output, CRC, trigger effects, the 0x32 haptics and 0x39 speaker reports, the DualShock 4 0x17 audio report and its SBC encoder, virtual-pad reports |
crates\dsz |
The app: Win32 HID (hid.rs), device threads (device.rs), output composer, input pipeline, haptics engine (haptics\, game audio in game.rs, DualShock 4 sound in ds4.rs), virtual controllers (virt\: USB/IP Xbox 360, DualShock 4, and DualSense, HidHide, default-audio guard, deadzones), profiles, UDP, game watcher, tray, egui UI (ui\) |
vendor\opus |
libopus 1.6 (BSD-3-Clause, see its COPYING), built into the exe by build.rs for the controller speaker |
vendor\eframe |
eframe 0.32.3 (MIT or Apache-2.0) with one fix: it busy-polled for a repaint of a hidden window, so the app used a full core in the tray (now under 1%) |
crates\dsz\defaults |
The profiles a fresh install starts with |
crates\dsz\assets |
The DualSense and DualSense Edge drawings the overview page, the sidebar mark and the app icon are drawn from: OpenController's pads.json looks (MIT, OpenController-LICENSE) |
cargo test --workspaceDSZ's protocol code was written from public documentation and code with open licenses:
| Source | License | Used for |
|---|---|---|
Linux hid-playstation |
GPL-2.0, read as documentation | Input and output report layouts, valid flags, status bits, CRC prefixes, feature reports, the DualShock 4 report, speaker and headset routing (volume, output path, preamp gain) and headset detection |
SDL SDL_hidapi_ps5.c |
zlib | Edge Fn and paddle buttons, the connection-state byte, improved rumble emulation |
| pydualsense | MIT | The Bluetooth 0x31 layout, player LED patterns and brightness, motor power, custom trigger modes |
| SAxense and godot-dualsense-native | MPL-2.0, MIT | The Bluetooth haptics report 0x32, the packet header's double-length bit, and Opus packet 0x16 for the headset jack (godot) |
Nielk1's PS5 Bluetooth capture (packet_samples.txt) |
Captured console traffic, used as data | The speaker report 0x39: config packet, two Opus frames (packet 0x15), two haptic blocks, and the frame counter |
| RFC 6716 and libopus's documentation | IETF, BSD-3-Clause | Reading the capture's Opus TOC byte (CELT, fullband, 10 ms, stereo) and the encoder settings |
Monado pssense_protocol.h |
Boost | The motor power reduction nibbles |
| dualsensectl | GPL-2.0, read as documentation | Rumble power as an attenuation of 0–7 |
| Nielk1's trigger effect generator | MIT | Adaptive trigger effect encoding |
| DS4Windows | GPL-3.0, read as documentation | The stick curve settings: dead zone, max zone, sensitivity curve, anti-dead zone, square stick, max output; rumble power in 12.5 % steps |
| Santroller's Xbox 360 controller notes | GPL-3.0, read as documentation | The layout of the virtual Xbox 360 controller's capability answers (buttons, triggers, sticks, rumble motors) |
| cemuhook protocol reference | public documentation | The DSU motion server: packets, messages, registration, the data layout |
DS4Windows UdpServer.cs/DS4Sixaxis.cs and evdevhook2 DeviceTypes.ini |
GPL-3.0, read as documentation | DSU motion axis signs and units for DualSense and DualShock 4 |
| dogtopus's descriptor dump and nondebug/dualsense | public dumps | The virtual DualSense's HID report descriptor and USB layout |
| DS4 Sound Link by abh1shekbana (u/Livid-Ad-2313 on Reddit) | MIT, code ported (notice in crates/ds-proto/DS4-Sound-Link-LICENSE) |
DualShock 4 Bluetooth audio: the SBC encoder, report 0x17 (four SBC frames, frame counter, speaker 0x02 / headphone 0x24 target), and the 0x11 audio fields (byte 2 0xA0, volume flags and bytes) |
| asimonf's DS4AudioStreamer | GPL-2.0, read as documentation | The 0x17 report layout, which DS4 Sound Link also follows |
| Bluetooth A2DP specification, SBC (Appendix B) | public specification | The SBC frame format, analysis filter window, bit allocation and CRC-8 that DS4 Sound Link's encoder follows |
hbashton's DS4Windows and VIIPER (makeAudioOnlyDescriptor) |
GPL-3.0, read as documentation | The approach for the microphone beside an Xbox 360 pad: a second USB device with the DualSense's identity and audio interfaces but no HID interface. DSZ's microphone device is built from its own virtual DualSense descriptors and the USB Audio Class 1.0 specification |
| stealth-alex's DualSense output report notes | public notes | Microphone source selection (audio flags bits 0–1); the microphone volume and mute fields match Linux hid-playstation |
DSZ is free. If it's useful to you and you'd like to support it, you can buy me a coffee on Ko-fi. Bug reports in Issues help just as much.
DSZ builds on the work of Jays2Kings, Ryochan7, Schmaldeo, hbashton, and the DS4Windows community, with thanks to VIIPER, HidHide, usbip-win2, and the wider controller-research community. The controller drawings and the app icon are drawn from OpenController's shapes, by Brunovncs (MIT). DualShock 4 speaker and headphone sound is ported from DS4 Sound Link, by abh1shekbana (u/Livid-Ad-2313 on Reddit, MIT).
MIT, see LICENSE. eframe in vendor\eframe is MIT or Apache-2.0 (the published crate, with one patch). libopus in vendor\opus is BSD-3-Clause. The controller drawings in crates\dsz\assets\pads.json are OpenController's (Brunovncs, MIT, crates\dsz\assets\OpenController-LICENSE). The SBC encoder and DualShock 4 audio reports in crates\ds-proto are ported from DS4 Sound Link (abh1shekbana, MIT, crates\ds-proto\DS4-Sound-Link-LICENSE). DualSense, DualSense Edge, DualShock, and PlayStation are trademarks of Sony Interactive Entertainment; this project is not affiliated with Sony.