Skip to content

DualSense Controller

VIIPER emulates complete USB-connected DualSense and DualSense Edge devices, including controls, touch, motion, adaptive triggers, lightbar, speaker, advanced haptics, and microphone endpoints.

All functions are part of the libVIIPER C API.

Device variants

Create function USB functions
CreateDualSenseDevice(...) DualSense HID, speaker/haptics OUT, microphone IN
CreateDualSenseEdgeDevice(...) DualSense Edge HID, speaker/haptics OUT, microphone IN
CreateDualSenseAudioOnlyDevice(...) DualSense speaker/haptics OUT and microphone IN sidecar (no HID gamepad)
CreateDualSenseEdgeAudioOnlyDevice(...) DualSense Edge speaker/haptics OUT and microphone IN sidecar (no HID gamepad)
CreateDualSenseGamepadOnlyDevice(...) DualSense HID gamepad only (no audio interfaces)
CreateDualSenseEdgeGamepadOnlyDevice(...) DualSense Edge HID gamepad only (no audio interfaces)

All create functions share the signature (serverHandle, &handle, busID, autoAttach, vid, pid, meta). The audio-only variants are useful when the gamepad is emulated by another device and Windows should only see the speaker/microphone endpoints.

API

Function Description
CreateDualSenseDevice / CreateDualSenseEdgeDevice Create a virtual DualSense / Edge (HID + audio)
CreateDualSenseAudioOnlyDevice / CreateDualSenseEdgeAudioOnlyDevice Create an audio-only sidecar
CreateDualSenseGamepadOnlyDevice / CreateDualSenseEdgeGamepadOnlyDevice Create a gamepad-only device
SetDualSenseDeviceState(handle, state) Push an input state to the device
SetDualSenseMetaState(handle, meta) Merge-update identity/battery/connection state at runtime
SetDualSenseOutputCallback(handle, cb) Register a callback for rumble, LEDs and player LEDs
SetDualSenseOutputStateCallback(handle, cb) Register a callback for the full output state
SetDualSenseRealtimeHapticsCallback(handle, cb) Register a callback for low-latency rear haptics
SetDualSenseSpeakerResetCallback(handle, cb) Register a callback for speaker-stream resets
SetDualSenseAudioOutCallback(handle, cb) Register a callback for haptics/speaker PCM from the host
SetDualSenseMicrophonePCM(handle, data, length) Queue a microphone PCM frame (1920 bytes)
RemoveDualSenseDevice(handle) Remove the device

Only one output callback may be active at a time; the most recently installed wins.

Input state

typedef struct {
    int8_t   LX;
    int8_t   LY;
    int8_t   RX;
    int8_t   RY;
    uint32_t Buttons;
    uint8_t  DPad;
    uint8_t  L2;
    uint8_t  R2;
    uint16_t Touch1X;
    uint16_t Touch1Y;
    uint8_t  Touch1Active;
    uint8_t  Touch1Tracking;
    uint16_t Touch2X;
    uint16_t Touch2Y;
    uint8_t  Touch2Active;
    uint8_t  Touch2Tracking;
    int16_t  GyroX;
    int16_t  GyroY;
    int16_t  GyroZ;
    int16_t  AccelX;
    int16_t  AccelY;
    int16_t  AccelZ;
} DSDeviceState;

Button bits:

Control Value
Square 0x00000010
Cross 0x00000020
Circle 0x00000040
Triangle 0x00000080
L1 / R1 0x00000100 / 0x00000200
L2 / R2 0x00000400 / 0x00000800
Create / Options 0x00001000 / 0x00002000
L3 / R3 0x00004000 / 0x00008000
PS 0x00010000
Touchpad click 0x00020000
Mic mute 0x00040000
Edge LFn / RFn 0x00100000 / 0x00200000
Edge L4 / R4 0x00400000 / 0x00800000

D-pad bits are Up 0x01, Down 0x02, Left 0x04, Right 0x08.

Meta state

Metadata passed to the create functions and SetDualSenseMetaState. Fields left at their zero value (NULL/0) keep the current value, so a partial update only changes what the caller supplies.

typedef struct {
    const char* SerialNumber;       // NULL = use default
    const char* MACAddress;         // NULL = use default
    const char* Board;              // NULL = use default
    uint8_t     BatteryStatus;      // 0 = use default
    double      TemperatureCelsius; // 0 = use default
    double      BatteryVoltage;     // 0 = use default
    const char* ShellColor;         // NULL = use default (2-char code, e.g. "00", "Z1")
    const char* BuildTime;          // NULL = use default (RFC3339 or "YYYY-MM-DD HH:MM:SS")
    uint8_t     ConnectionStatus;   // 0 = use default (DS_CONNECTION_* flags)
} DSMetaState;

Connection status flags

The input-report connection status byte. The virtual device always reports USB data/power; the headphone/mic/mute flags are application-controlled.

Constant Value
DS_CONNECTION_HEADPHONE 0x01
DS_CONNECTION_MIC 0x02
DS_CONNECTION_MIC_MUTED 0x04
DS_CONNECTION_USB_DATA 0x08
DS_CONNECTION_USB_POWER 0x10

Shell colors

DS_SHELL_COLOR_* constants are defined for all hardware variants, e.g. DS_SHELL_COLOR_WHITE ("00"), DS_SHELL_COLOR_BLACK ("01"), DS_SHELL_COLOR_GOD_OF_WAR_RAGNAROK ("Z1"), DS_SHELL_COLOR_ASTRO_BOT ("Z3"), DS_SHELL_COLOR_GENSHIN_IMPACT ("ZE"), and more — see libVIIPER.h.

Output callback

The basic callback reports rumble, lightbar color, and player LEDs:

typedef void (*DSOutputCallback)(
    DSDeviceHandle handle,
    uint8_t rumbleSmall,
    uint8_t rumbleLarge,
    uint8_t ledRed,
    uint8_t ledGreen,
    uint8_t ledBlue,
    uint8_t playerLeds
);

Output state callback

SetDualSenseOutputStateCallback delivers the full output state, including the adaptive-trigger blocks for R2 and L2, the native 48-byte USB output report, and the 398-byte Bluetooth combined carrier:

typedef struct {
    uint8_t RumbleSmall;
    uint8_t RumbleLarge;
    uint8_t LedRed;
    uint8_t LedGreen;
    uint8_t LedBlue;
    uint8_t PlayerLeds;
    uint8_t TriggerR2Mode;
    uint8_t TriggerR2StartResistance;
    uint8_t TriggerR2EffectForce;
    uint8_t TriggerR2RangeForce;
    uint8_t TriggerR2NearReleaseStrength;
    uint8_t TriggerR2NearMiddleStrength;
    uint8_t TriggerR2PressedStrength;
    uint8_t TriggerR2Frequency;
    // ... mirror fields for TriggerL2*
    uint8_t RawOutputReport[48];            // native USB output report 0x02
    uint8_t BluetoothCombinedOutputReport[398];
    uint8_t MicLed;             // 0=off, 1=on, 2=pulse
    uint8_t LightbarSetup;      // 0x01 default, 0x02 custom
    uint8_t LightbarBrightness; // 0=high, 1=medium, 2=low
} DSOutputState;

typedef void (*DSOutputStateCallback)(DSDeviceHandle handle, DSOutputState output);

Realtime haptics callback

SetDualSenseRealtimeHapticsCallback is invoked as soon as a rear haptics interval completes (every 512 source frames ≈ 93.75 Hz), before the next 480-frame speaker boundary. This is the low-latency haptics lane; the delivered output state carries a fresh BluetoothCombinedOutputReport.

The 398-byte combined Bluetooth carrier (report ID 0x36) bundles the output state, the haptics sample, and the speaker lane in one HID report:

Offset Length Content
0 1 Report ID 0x36
1 1 Rolling sequence nibble ((seq & 0x0F) << 4)
2 9 Packet 0x11 header: 0x91 0x07 0xFE + 5 queue-length bytes + seq
11 1 0x90 — packet 0x10 (output state) marker
12 1 63 — state length
13 63 Output state (mirrors USB output report bytes 1-47 when set)
76 1 0x92 — packet 0x12 (haptics) marker
77 1 64 — haptics sample length
78 64 Haptics sample: signed 8-bit stereo, 32 frames @ 3 kHz
142 1 0x93 — packet 0x13 (speaker) marker
143 1 0 — speaker length (empty in VIIPER)
394 4 CRC-32 (little-endian) over bytes 0-393 — custom seed
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include "libVIIPER.h"

void realtimeHapticsCallback(DSDeviceHandle handle, DSOutputState output) {
    (void)handle;
    const uint8_t* report = output.BluetoothCombinedOutputReport;

    // Guard against a carrier without a fresh haptics sample.
    if (report[0] != 0x36 || report[76] != 0x92 || report[77] != 64)
        return;

    // 64 signed 8-bit samples, stereo-interleaved (L/R): 32 per channel.
    const int8_t* haptics = (const int8_t*)&report[78];
    double l2 = 0.0, r2 = 0.0;
    for (int i = 0; i < 32; i++) {
        l2 += (double)haptics[i * 2] * haptics[i * 2];     // rear-left actuator
        r2 += (double)haptics[i * 2 + 1] * haptics[i * 2 + 1]; // rear-right
    }
    double lRms = sqrt(l2 / 32) / 127.0; // normalize to 0..1
    double rRms = sqrt(r2 / 32) / 127.0;
    printf("<- Realtime haptics: L=%.0f%% R=%.0f%%\n", lRms * 100.0, rRms * 100.0);

    // Rumble, adaptive triggers and lightbar are decoded struct fields — no
    // need to parse bytes 13..75 yourself.
}

See the C# Bindings page for the same decode in C#.

Audio

Speaker / haptics PCM out

SetDualSenseAudioOutCallback receives the raw bytes the host wrote to the haptics audio-out endpoint: four S16LE channels at 48 kHz (front stereo + rear haptics). The buffer is only valid during the call.

SetDualSenseSpeakerResetCallback is invoked when the haptics audio interface alternate setting changes or the endpoint is reset. Treat it as a stream generation barrier: flush queued speaker PCM and haptics.

Microphone in

SetDualSenseMicrophonePCM queues a microphone PCM frame captured from the host-facing mic stream. The frame must be exactly 1920 bytes (480 frames of two S16LE channels at 48 kHz).