C# Bindings
libVIIPER is a pure C shared library (libVIIPER.dll on Windows, libVIIPER.so on
Linux). From C# you call it through P/Invoke — there is no managed wrapper
package for the embedded library, but the complete set of declarations is small,
stable, and documented below.
Two ways to use VIIPER from C#
- libVIIPER (this page) — embed the whole USB/USBIP stack in your process via P/Invoke.
- Viiper.Client — a generated TCP client library that talks to a standalone
viiper serverover the network. See Client Libraries.
Project setup
Create a regular .NET console/library project and copy the native library next to your output:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<None Include="..\..\dist\libVIIPER\libVIIPER.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
<None Include="..\..\dist\libVIIPER\libVIIPER.so">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
A complete runnable project lives at
examples/libVIIPER/csharp/.
General P/Invoke rules
Apply these to every declaration in this reference:
[DllImport("libVIIPER", CallingConvention = CallingConvention.Cdecl)]on every function.boolreturn values andboolparameters need[MarshalAs(UnmanagedType.I1)].- Handles (
USBServerHandle,Xbox360DeviceHandle, …) are opaqueuintptr_tvalues → usenuint. size_tparameters → usenuint.- Strings inside config/meta structs are
char*→[MarshalAs(UnmanagedType.LPStr)] string?. - Callback delegates need
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]. - Keep delegate instances alive in fields or local variables for the lifetime of
the server/device they are registered on — the GC cannot see references held by
native code. Registering a method group inline (
SetXbox360RumbleCallback(handle, RumbleCallback)) creates a temporary delegate that can be collected at any time. - Structs passed by value must mirror the C layout exactly with
[StructLayout(LayoutKind.Sequential)]. - Callbacks are invoked from the USBIP server's background threads. Keep them short and thread-safe, and never call into libVIIPER from inside a callback. PCM buffers passed to audio callbacks are only valid during the call — copy the data if you need to keep it.
Constants
Log levels
Xbox 360 buttons
static class Xbox360Buttons
{
public const uint DPadUp = 0x0001;
public const uint DPadDown = 0x0002;
public const uint DPadLeft = 0x0004;
public const uint DPadRight = 0x0008;
public const uint Start = 0x0010;
public const uint Back = 0x0020;
public const uint LThumb = 0x0040;
public const uint RThumb = 0x0080;
public const uint LShoulder = 0x0100;
public const uint RShoulder = 0x0200;
public const uint Guide = 0x0400;
public const uint A = 0x1000;
public const uint B = 0x2000;
public const uint X = 0x4000;
public const uint Y = 0x8000;
}
DualShock 4 buttons & D-pad
static class DS4Buttons
{
public const ushort Square = 0x0010;
public const ushort Cross = 0x0020;
public const ushort Circle = 0x0040;
public const ushort Triangle = 0x0080;
public const ushort L1 = 0x0100;
public const ushort R1 = 0x0200;
public const ushort L2 = 0x0400;
public const ushort R2 = 0x0800;
public const ushort Share = 0x1000;
public const ushort Options = 0x2000;
public const ushort L3 = 0x4000;
public const ushort R3 = 0x8000;
public const ushort PS = 0x0001;
public const ushort Touchpad = 0x0002;
}
static class DS4DPad
{
public const byte Up = 0x01;
public const byte Down = 0x02;
public const byte Left = 0x04;
public const byte Right = 0x08;
}
DS4 D-pad encoding
DS4DeviceState.DPad is a bitmask (the values above), which is what the
DS4DPad class here and the docs/devices/dualshock4.md
reference use. Be careful not to confuse it with the DS4_DPAD_* constants in
libVIIPER.h (e.g. DS4_DPAD_UP = 0x00, DS4_DPAD_NEUTRAL = 0x08) — those are
the USB hat-switch encoding used inside the report and must not be OR'd into
the field.
DualSense buttons & D-pad
static class DSButtons
{
public const uint Square = 0x00000010;
public const uint Cross = 0x00000020;
public const uint Circle = 0x00000040;
public const uint Triangle = 0x00000080;
public const uint L1 = 0x00000100;
public const uint R1 = 0x00000200;
public const uint L2 = 0x00000400;
public const uint R2 = 0x00000800;
public const uint Create = 0x00001000;
public const uint Options = 0x00002000;
public const uint L3 = 0x00004000;
public const uint R3 = 0x00008000;
public const uint PS = 0x00010000;
public const uint Touchpad = 0x00020000;
public const uint MicMute = 0x00040000;
public const uint LFn = 0x00100000; // Edge
public const uint RFn = 0x00200000; // Edge
public const uint L4 = 0x00400000; // Edge
public const uint R4 = 0x00800000; // Edge
}
static class DSDPad
{
public const byte Up = 0x01;
public const byte Down = 0x02;
public const byte Left = 0x04;
public const byte Right = 0x08;
}
static class DSConnection
{
public const byte Headphone = 0x01;
public const byte Mic = 0x02;
public const byte MicMuted = 0x04;
public const byte USBData = 0x08;
public const byte USBPower = 0x10;
}
static class DSShellColors
{
public const string White = "00";
public const string Black = "01";
public const string CosmicRed = "02";
public const string NovaPink = "03";
public const string GalacticPurple = "04";
public const string StarlightBlue = "05";
public const string GreyCamouflage = "06";
public const string VolcanicRed = "07";
public const string SterlingSilver = "08";
public const string CobaltBlue = "09";
public const string ChromaTeal = "10";
public const string ChromaIndigo = "11";
public const string ChromaPearl = "12";
public const string Anniversary30th = "30";
public const string GodOfWarRagnarok = "Z1";
public const string SpiderMan2 = "Z2";
public const string AstroBot = "Z3";
public const string Fortnite = "Z4";
public const string MonsterHunterWilds = "Z5";
public const string TheLastOfUs = "Z6";
public const string GhostOfYotei = "Z7";
public const string IconBlueLimitedEdition = "ZB";
public const string AstroBotJoyfulEdition = "ZC";
public const string GenshinImpact = "ZE";
}
Switch 2 Pro buttons
static class NS2ProButtons
{
public const uint B = 0x00000001;
public const uint A = 0x00000002;
public const uint Y = 0x00000004;
public const uint X = 0x00000008;
public const uint R = 0x00000010;
public const uint ZR = 0x00000020;
public const uint Plus = 0x00000040;
public const uint RightStick = 0x00000080;
public const uint Down = 0x00000100;
public const uint Right = 0x00000200;
public const uint Left = 0x00000400;
public const uint Up = 0x00000800;
public const uint L = 0x00001000;
public const uint ZL = 0x00002000;
public const uint Minus = 0x00004000;
public const uint LeftStick = 0x00008000;
public const uint Home = 0x00010000;
public const uint Capture = 0x00020000;
public const uint GR = 0x00040000;
public const uint GL = 0x00080000;
public const uint C = 0x00100000;
public const uint Headset = 0x00200000;
}
static class NS2ProSticks
{
public const ushort Min = 0x0000;
public const ushort Center = 0x0800;
public const ushort Max = 0x0FFF;
}
Keyboard modifiers & LEDs
static class KBModifiers
{
public const byte LeftCtrl = 0x01;
public const byte LeftShift = 0x02;
public const byte LeftAlt = 0x04;
public const byte LeftGUI = 0x08;
public const byte RightCtrl = 0x10;
public const byte RightShift = 0x20;
public const byte RightAlt = 0x40;
public const byte RightGUI = 0x80;
}
static class KBLEDs
{
public const byte NumLock = 0x01;
public const byte CapsLock = 0x02;
public const byte ScrollLock = 0x04;
public const byte Compose = 0x08;
public const byte Kana = 0x10;
}
Mouse buttons
static class MouseButtons
{
public const byte Left = 0x01;
public const byte Right = 0x02;
public const byte Middle = 0x04;
public const byte Back = 0x08;
public const byte Forward = 0x10;
}
Keyboard key codes
KB_KEY_* constants are USB HID usage codes (page 0x07, Keyboard/Keypad). Set
bits in KeyboardDeviceState.KeyBitmap at the index of the key code divided by 8
(KeyBitmap[code / 8] |= (byte)(1 << (code % 8))).
static class KBKeys
{
public const byte A = 0x04; public const byte B = 0x05;
public const byte C = 0x06; public const byte D = 0x07;
public const byte E = 0x08; public const byte F = 0x09;
public const byte G = 0x0A; public const byte H = 0x0B;
public const byte I = 0x0C; public const byte J = 0x0D;
public const byte K = 0x0E; public const byte L = 0x0F;
public const byte M = 0x10; public const byte N = 0x11;
public const byte O = 0x12; public const byte P = 0x13;
public const byte Q = 0x14; public const byte R = 0x15;
public const byte S = 0x16; public const byte T = 0x17;
public const byte U = 0x18; public const byte V = 0x19;
public const byte W = 0x1A; public const byte X = 0x1B;
public const byte Y = 0x1C; public const byte Z = 0x1D;
public const byte Digit1 = 0x1E; public const byte Digit2 = 0x1F;
public const byte Digit3 = 0x20; public const byte Digit4 = 0x21;
public const byte Digit5 = 0x22; public const byte Digit6 = 0x23;
public const byte Digit7 = 0x24; public const byte Digit8 = 0x25;
public const byte Digit9 = 0x26; public const byte Digit0 = 0x27;
public const byte Enter = 0x28; public const byte Escape = 0x29;
public const byte Backspace = 0x2A; public const byte Tab = 0x2B;
public const byte Space = 0x2C; public const byte Minus = 0x2D;
public const byte Equal = 0x2E; public const byte LeftBrace = 0x2F;
public const byte RightBrace = 0x30; public const byte Backslash = 0x31;
public const byte Semicolon = 0x33; public const byte Apostrophe = 0x34;
public const byte Grave = 0x35; public const byte Comma = 0x36;
public const byte Period = 0x37; public const byte Slash = 0x38;
public const byte CapsLock = 0x39;
public const byte F1 = 0x3A; public const byte F2 = 0x3B;
public const byte F3 = 0x3C; public const byte F4 = 0x3D;
public const byte F5 = 0x3E; public const byte F6 = 0x3F;
public const byte F7 = 0x40; public const byte F8 = 0x41;
public const byte F9 = 0x42; public const byte F10 = 0x43;
public const byte F11 = 0x44; public const byte F12 = 0x45;
public const byte PrintScreen = 0x46; public const byte ScrollLock = 0x47;
public const byte Pause = 0x48; public const byte Insert = 0x49;
public const byte Home = 0x4A; public const byte PageUp = 0x4B;
public const byte Delete = 0x4C; public const byte End = 0x4D;
public const byte PageDown = 0x4E;
public const byte Right = 0x4F; public const byte Left = 0x50;
public const byte Down = 0x51; public const byte Up = 0x52;
public const byte NumLock = 0x53; public const byte KeypadSlash = 0x54;
public const byte KeypadAsterisk = 0x55; public const byte KeypadMinus = 0x56;
public const byte KeypadPlus = 0x57; public const byte KeypadEnter = 0x58;
public const byte Keypad1 = 0x59; public const byte Keypad2 = 0x5A;
public const byte Keypad3 = 0x5B; public const byte Keypad4 = 0x5C;
public const byte Keypad5 = 0x5D; public const byte Keypad6 = 0x5E;
public const byte Keypad7 = 0x5F; public const byte Keypad8 = 0x60;
public const byte Keypad9 = 0x61; public const byte Keypad0 = 0x62;
public const byte KeypadDot = 0x63;
public const byte Mute = 0x7F; public const byte VolumeUp = 0x80;
public const byte VolumeDown = 0x81;
}
See the USB HID Usage Tables for the canonical key-code assignments.
Structs
[StructLayout(LayoutKind.Sequential)]
struct USBServerConfig
{
[MarshalAs(UnmanagedType.LPStr)]
public string? addr; // default "0.0.0.0:3241"
public ulong connection_timeout_ms; // default 30000 (30s)
public ulong device_handler_connect_timeout_ms; // default 5000 (5s)
public uint write_batch_flush_interval_ms; // default 1 (1ms)
}
All device state structs are passed by value to Set<Device>DeviceState.
Zeroed fields select defaults; a zeroed meta struct is equivalent to passing NULL.
[StructLayout(LayoutKind.Sequential)]
struct Xbox360DeviceState
{
public uint Buttons;
public byte LT;
public byte RT;
public short LX;
public short LY;
public short RX;
public short RY;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] Reserved;
}
[StructLayout(LayoutKind.Sequential)]
struct DS4DeviceState
{
public sbyte LX, LY, RX, RY;
public ushort Buttons;
public byte DPad;
public byte L2, R2;
public ushort Touch1X, Touch1Y;
public byte Touch1Active;
public ushort Touch2X, Touch2Y;
public byte Touch2Active;
public short GyroX, GyroY, GyroZ;
public short AccelX, AccelY, AccelZ;
}
[StructLayout(LayoutKind.Sequential)]
struct DSDeviceState
{
public sbyte LX, LY, RX, RY;
public uint Buttons;
public byte DPad;
public byte L2, R2;
public ushort Touch1X, Touch1Y;
public byte Touch1Active, Touch1Tracking;
public ushort Touch2X, Touch2Y;
public byte Touch2Active, Touch2Tracking;
public short GyroX, GyroY, GyroZ;
public short AccelX, AccelY, AccelZ;
}
[StructLayout(LayoutKind.Sequential)]
struct NS2ProDeviceState
{
public uint Buttons;
public ushort LX, LY, RX, RY;
public short AccelX, AccelY, AccelZ;
public short GyroX, GyroY, GyroZ;
}
[StructLayout(LayoutKind.Sequential)]
struct KeyboardDeviceState
{
public byte Modifiers;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] KeyBitmap; // 256-bit bitmap, one bit per HID key code
}
[StructLayout(LayoutKind.Sequential)]
struct MouseDeviceState
{
public byte Buttons;
public short DX, DY;
public short Wheel, Pan;
}
Meta structs are passed by pointer; fields left at zero value keep defaults:
[StructLayout(LayoutKind.Sequential)]
struct DS4MetaState
{
[MarshalAs(UnmanagedType.LPStr)] public string? SerialNumber;
[MarshalAs(UnmanagedType.LPStr)] public string? Board;
public byte BatteryStatus;
public double TemperatureCelsius;
public double BatteryVoltage;
[MarshalAs(UnmanagedType.LPStr)] public string? BuildTime; // RFC3339 or "YYYY-MM-DD HH:MM:SS"
}
[StructLayout(LayoutKind.Sequential)]
struct DSMetaState
{
[MarshalAs(UnmanagedType.LPStr)] public string? SerialNumber;
[MarshalAs(UnmanagedType.LPStr)] public string? MACAddress;
[MarshalAs(UnmanagedType.LPStr)] public string? Board;
public byte BatteryStatus;
public double TemperatureCelsius;
public double BatteryVoltage;
[MarshalAs(UnmanagedType.LPStr)] public string? ShellColor; // 2-char code, e.g. "00", "Z1"
[MarshalAs(UnmanagedType.LPStr)] public string? BuildTime;
public byte ConnectionStatus; // DS_CONNECTION_* flags
}
[StructLayout(LayoutKind.Sequential)]
struct NS2ProMetaState
{
[MarshalAs(UnmanagedType.LPStr)] public string? SerialNumber;
public byte BatteryLevel; // 0-9; 0 = use default
public byte Charging; // 0 = not charging
public byte ExternalPower; // 0 = battery only
public ushort BatteryVolts; // mV; 0 = use default
}
The full DualSense output state (delivered by the output-state and realtime-haptics callbacks) is passed by value:
[StructLayout(LayoutKind.Sequential)]
struct DSOutputState
{
public byte RumbleSmall;
public byte RumbleLarge;
public byte LedRed, LedGreen, LedBlue;
public byte PlayerLeds;
public byte TriggerR2Mode;
public byte TriggerR2StartResistance;
public byte TriggerR2EffectForce;
public byte TriggerR2RangeForce;
public byte TriggerR2NearReleaseStrength;
public byte TriggerR2NearMiddleStrength;
public byte TriggerR2PressedStrength;
public byte TriggerR2Frequency;
public byte TriggerL2Mode;
public byte TriggerL2StartResistance;
public byte TriggerL2EffectForce;
public byte TriggerL2RangeForce;
public byte TriggerL2NearReleaseStrength;
public byte TriggerL2NearMiddleStrength;
public byte TriggerL2PressedStrength;
public byte TriggerL2Frequency;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)]
public byte[] RawOutputReport; // native USB output report 0x02
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 398)]
public byte[] BluetoothCombinedOutputReport; // V5 carrier 0x36; 64-byte haptics sample at offset 78 (see audio example)
public byte MicLed; // 0=off, 1=on, 2=pulse
public byte LightbarSetup; // 0x01 default, 0x02 custom
public byte LightbarBrightness; // 0=high, 1=medium, 2=low
}
[StructLayout(LayoutKind.Sequential)]
struct NS2ProOutputState
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] LeftRumble;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] RightRumble;
public byte Flags; // bit 0 = rumble update, bit 1 = player LED update
public byte PlayerLedMask;
}
Callback delegates
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void VIIPERLogCallbackDelegate(VIIPERLogLevel level, [MarshalAs(UnmanagedType.LPStr)] string message);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void Xbox360RumbleCallbackDelegate(nuint handle, byte leftMotor, byte rightMotor);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void KeyboardLEDCallbackDelegate(nuint handle, byte leds);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DS4OutputCallbackDelegate(nuint handle, byte rumbleSmall, byte rumbleLarge,
byte ledRed, byte ledGreen, byte ledBlue, byte flashOn, byte flashOff);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DS4SpeakerCallbackDelegate(nuint handle, IntPtr pcm, nuint length);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DS4SpeakerResetCallbackDelegate(nuint handle);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSOutputCallbackDelegate(nuint handle, byte rumbleSmall, byte rumbleLarge,
byte ledRed, byte ledGreen, byte ledBlue, byte playerLeds);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSOutputStateCallbackDelegate(nuint handle, DSOutputState output);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSRealtimeHapticsCallbackDelegate(nuint handle, DSOutputState output);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSSpeakerResetCallbackDelegate(nuint handle);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSAudioCallbackDelegate(nuint handle, IntPtr pcm, nuint length);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void NS2ProOutputCallbackDelegate(nuint handle, NS2ProOutputState output);
PCM callbacks (DS4SpeakerCallbackDelegate, DSAudioCallbackDelegate) receive a raw
pointer that is only valid during the call. Copy it with Marshal.Copy:
DS4SpeakerCallbackDelegate speakerCb = (handle, pcm, length) =>
{
var buf = new byte[(int)length];
Marshal.Copy(pcm, buf, 0, buf.Length);
// use buf ...
};
P/Invoke declarations
Server lifecycle
static class LibVIIPER
{
const string Lib = "libVIIPER";
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool NewUSBServer([In] ref USBServerConfig config,
out nuint outHandle, VIIPERLogCallbackDelegate? logCallback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CloseUSBServer(nuint handle);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateUSBBus(nuint serverHandle, ref uint busID);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool RemoveUSBBus(nuint serverHandle, uint busID);
Xbox 360
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateXbox360Device(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct, byte xinputSubType);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetXbox360DeviceState(nuint deviceHandle, Xbox360DeviceState state);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetXbox360RumbleCallback(nuint deviceHandle, Xbox360RumbleCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool RemoveXbox360Device(nuint deviceHandle);
DualShock 4
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateDS4Device(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct, ref DS4MetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDS4DeviceState(nuint deviceHandle, DS4DeviceState state);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDS4MetaState(nuint deviceHandle, ref DS4MetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDS4OutputCallback(nuint deviceHandle, DS4OutputCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDS4SpeakerCallback(nuint deviceHandle, DS4SpeakerCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDS4SpeakerResetCallback(nuint deviceHandle, DS4SpeakerResetCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDS4MicrophonePCM(nuint deviceHandle, [In] byte[] data, nuint length);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool RemoveDS4Device(nuint deviceHandle);
DualSense (and Edge)
// Variants: Device, EdgeDevice, AudioOnlyDevice, EdgeAudioOnlyDevice,
// GamepadOnlyDevice, EdgeGamepadOnlyDevice
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateDualSenseDevice(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct, ref DSMetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateDualSenseEdgeDevice(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct, ref DSMetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateDualSenseAudioOnlyDevice(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct, ref DSMetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateDualSenseEdgeAudioOnlyDevice(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct, ref DSMetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateDualSenseGamepadOnlyDevice(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct, ref DSMetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateDualSenseEdgeGamepadOnlyDevice(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct, ref DSMetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDualSenseDeviceState(nuint deviceHandle, DSDeviceState state);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDualSenseMetaState(nuint deviceHandle, ref DSMetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDualSenseOutputCallback(nuint deviceHandle, DSOutputCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDualSenseOutputStateCallback(nuint deviceHandle, DSOutputStateCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDualSenseRealtimeHapticsCallback(nuint deviceHandle, DSRealtimeHapticsCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDualSenseSpeakerResetCallback(nuint deviceHandle, DSSpeakerResetCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDualSenseAudioOutCallback(nuint deviceHandle, DSAudioCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetDualSenseMicrophonePCM(nuint deviceHandle, [In] byte[] data, nuint length);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool RemoveDualSenseDevice(nuint deviceHandle);
Switch 2 Pro
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateNS2ProDevice(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct, ref NS2ProMetaState meta);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetNS2ProDeviceState(nuint deviceHandle, NS2ProDeviceState state);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetNS2ProOutputCallback(nuint deviceHandle, NS2ProOutputCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool RemoveNS2ProDevice(nuint deviceHandle);
Keyboard
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateKeyboardDevice(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetKeyboardDeviceState(nuint deviceHandle, KeyboardDeviceState state);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetKeyboardLEDCallback(nuint deviceHandle, KeyboardLEDCallbackDelegate? callback);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool RemoveKeyboardDevice(nuint deviceHandle);
Mouse
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateMouseDevice(nuint serverHandle, out nuint outDeviceHandle,
uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
ushort idVendor, ushort idProduct);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool SetMouseDeviceState(nuint deviceHandle, MouseDeviceState state);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool RemoveMouseDevice(nuint deviceHandle);
}
Optional meta parameters
Create/meta functions taking ref DS4MetaState / ref DSMetaState /
ref NS2ProMetaState accept NULL in C. In C#, pass a zeroed struct — every
zeroed field selects the device default, which is exactly what NULL means.
Complete example
using System.Runtime.InteropServices;
class Program
{
static void LogCallback(VIIPERLogLevel level, string message)
=> Console.WriteLine($"libVIIPER [{level}] {message}");
static void RumbleCallback(nuint handle, byte leftMotor, byte rightMotor)
=> Console.WriteLine($"<- Rumble: Left={leftMotor}, Right={rightMotor}");
static int Main()
{
// Keep delegates alive for the lifetime of the server/device!
VIIPERLogCallbackDelegate logCb = LogCallback;
USBServerConfig conf = new() { addr = "localhost:3245" };
if (!LibVIIPER.NewUSBServer(ref conf, out nuint serverHandle, logCb))
return 1;
uint busID = 0;
LibVIIPER.CreateUSBBus(serverHandle, ref busID);
if (!LibVIIPER.CreateXbox360Device(serverHandle, out nuint deviceHandle, busID,
autoAttachLocalhost: true, 0, 0, 0))
return 1;
Xbox360RumbleCallbackDelegate rumbleCb = RumbleCallback;
LibVIIPER.SetXbox360RumbleCallback(deviceHandle, rumbleCb);
Xbox360DeviceState state = new()
{
Buttons = Xbox360Buttons.A,
LT = 128,
LX = 20000,
};
LibVIIPER.SetXbox360DeviceState(deviceHandle, state);
Console.WriteLine("Press Enter to exit...");
Console.ReadLine();
LibVIIPER.CloseUSBServer(serverHandle);
return 0;
}
}
DualSense audio example
The DualSense exposes a full audio stack: the host streams speaker/haptics PCM to
an audio-out callback, and you feed captured microphone PCM back to the device.
All PCM callbacks receive a raw pointer that is only valid during the call,
so copy the data with Marshal.Copy before using it elsewhere.
// Keep all delegates alive for the lifetime of the device!
DSAudioCallbackDelegate audioOutCb = (handle, pcm, length) =>
{
// 4 channels S16LE @ 48 kHz: front L/R + rear haptics L/R
var bytes = new byte[(int)length];
Marshal.Copy(pcm, bytes, 0, bytes.Length);
Console.WriteLine($"<- Audio out: {bytes.Length} bytes");
};
DSSpeakerResetCallbackDelegate speakerResetCb = handle =>
{
// Stream generation barrier: flush queued speaker PCM / haptics here.
Console.WriteLine("<- Speaker stream reset");
};
DSOutputStateCallbackDelegate outputStateCb = (handle, output) =>
{
Console.WriteLine($"<- Rumble: {output.RumbleSmall}/{output.RumbleLarge}, " +
$"LED: #{output.LedRed:X2}{output.LedGreen:X2}{output.LedBlue:X2}, " +
$"Players: 0x{output.PlayerLeds:X2}");
// output.RawOutputReport (48 bytes) and
// output.BluetoothCombinedOutputReport (398 bytes) are also available —
// see the realtime haptics callback below for decoding the latter.
};
DSMetaState meta = new() { SerialNumber = "MY-DS-0001" };
if (!LibVIIPER.CreateDualSenseDevice(serverHandle, out nuint dsHandle, busID,
autoAttachLocalhost: true, 0, 0, ref meta))
return 1;
LibVIIPER.SetDualSenseAudioOutCallback(dsHandle, audioOutCb);
LibVIIPER.SetDualSenseSpeakerResetCallback(dsHandle, speakerResetCb);
LibVIIPER.SetDualSenseOutputStateCallback(dsHandle, outputStateCb);
// Capture host-facing mic audio and feed it to the virtual device.
// The frame must be exactly 1920 bytes (480 frames of 2ch S16LE @ 48 kHz);
// the call returns false for any other length.
var micFrame = new byte[1920];
// ... fill micFrame from your audio capture ...
LibVIIPER.SetDualSenseMicrophonePCM(dsHandle, micFrame, (nuint)micFrame.Length);
// The V5 combined Bluetooth carrier is a single 398-byte HID report
// (report ID 0x36) that bundles the output state, the haptics sample, and the
// speaker lane:
//
// [0] report ID (0x36)
// [1] rolling sequence nibble ((seq & 0x0F) << 4)
// [2..10] packet 0x11 header: 0x91 0x07 0xFE + 5 queue-length bytes + seq
// [11] 0x90 (packet 0x10: output state marker)
// [12] 63 (state length)
// [13..75] output state (mirrors USB output report bytes 1-47 when set)
// [76] 0x92 (packet 0x12: haptics marker)
// [77] 64 (haptics sample length)
// [78..141] haptics sample: signed 8-bit stereo, 32 frames @ 3 kHz
// [142] 0x93 (packet 0x13: speaker marker)
// [143] 0 (speaker length — empty in VIIPER)
// [394..397] CRC-32 (little-endian) over bytes 0..393 — custom seed, not
// standard IEEE CRC-32; do not verify it with a generic CRC-32 library
// Low-latency haptics lane: fires as soon as a 512-frame haptics interval
// completes (~93.75 Hz) and carries the fresh 64-byte haptics sample embedded
// in the combined carrier above.
DSRealtimeHapticsCallbackDelegate realtimeHapticsCb = (handle, output) =>
{
byte[] report = output.BluetoothCombinedOutputReport;
if (report.Length < 142 || report[0] != 0x36)
return; // not a V5 combined carrier — nothing to decode
// Sanity-check the packet 0x12 marker and length before decoding.
if (report[76] != 0x92 || report[77] != 64)
return;
// 64 signed 8-bit samples, stereo-interleaved (L/R): 32 per channel.
Span<byte> raw = report.AsSpan(78, 64);
Span<int> left = stackalloc int[32];
Span<int> right = stackalloc int[32];
double l2 = 0, r2 = 0;
for (int i = 0; i < 32; i++)
{
left[i] = (sbyte)raw[i * 2]; // rear-left haptics actuator
right[i] = (sbyte)raw[i * 2 + 1]; // rear-right haptics actuator
l2 += left[i] * left[i];
r2 += right[i] * right[i];
}
double lRms = Math.Sqrt(l2 / 32) / 127.0; // normalize to 0..1
double rRms = Math.Sqrt(r2 / 32) / 127.0;
Console.WriteLine($"<- Realtime haptics: L={lRms:P0} R={rRms:P0}");
// output.RumbleSmall/RumbleLarge, trigger effect fields and the lightbar
// are also decoded for you in the struct — no need to parse bytes 13..75.
};
LibVIIPER.SetDualSenseRealtimeHapticsCallback(dsHandle, realtimeHapticsCb);
// Also available: SetDualSenseOutputCallback (simple rumble/LED/player LEDs)
// and the audio-only / gamepad-only / Edge create variants. The Go-only
// atomic speaker+haptics carrier (SetAtomicAudioHapticsCallback) is not
// exposed by the C API — C# gets haptics via the audio-out callback above
// and this realtime lane.
DualShock 4 audio example
The DualShock 4 exposes the same speaker/microphone stack as the DualSense, at different sample rates: the speaker callback receives two S16LE channels at 32 kHz, and microphone frames must be exactly 320 bytes (160 frames of one S16LE channel at 16 kHz).
// Keep all delegates alive for the lifetime of the device!
DS4SpeakerCallbackDelegate speakerCb = (handle, pcm, length) =>
{
// 2 channels S16LE @ 32 kHz
var bytes = new byte[(int)length];
Marshal.Copy(pcm, bytes, 0, bytes.Length);
Console.WriteLine($"<- Speaker: {bytes.Length} bytes");
};
DS4SpeakerResetCallbackDelegate speakerResetCb = handle =>
{
// Stream generation barrier: flush queued speaker PCM here.
Console.WriteLine("<- Speaker stream reset");
};
DS4OutputCallbackDelegate outputCb = (handle, rumbleSmall, rumbleLarge,
ledRed, ledGreen, ledBlue, flashOn, flashOff) =>
{
Console.WriteLine($"<- Rumble: {rumbleSmall}/{rumbleLarge}, " +
$"LED: #{ledRed:X2}{ledGreen:X2}{ledBlue:X2}, Flash: {flashOn}/{flashOff}");
};
DS4MetaState meta = new() { SerialNumber = "MY-DS4-0001" };
if (!LibVIIPER.CreateDS4Device(serverHandle, out nuint ds4Handle, busID,
autoAttachLocalhost: true, 0, 0, ref meta))
return 1;
LibVIIPER.SetDS4SpeakerCallback(ds4Handle, speakerCb);
LibVIIPER.SetDS4SpeakerResetCallback(ds4Handle, speakerResetCb);
LibVIIPER.SetDS4OutputCallback(ds4Handle, outputCb);
// Feed captured mic audio to the virtual device. The frame must be exactly
// 320 bytes (160 frames of 1ch S16LE @ 16 kHz); the call returns false for
// any other length.
var micFrame = new byte[320];
// ... fill micFrame from your audio capture ...
LibVIIPER.SetDS4MicrophonePCM(ds4Handle, micFrame, (nuint)micFrame.Length);
Never call into libVIIPER from a callback
Audio/output callbacks run on the USBIP server's background threads. Do not
call Set* / Create* / Remove* functions from inside them — marshal the
work to your own thread instead.
See also the device reference pages for the exact semantics of every input state, meta state and callback: