-
Notifications
You must be signed in to change notification settings - Fork 6
DSU Protocol Implementation
PadForge implements the cemuhook DSU (DualShock UDP) protocol to stream controller motion data (gyroscope and accelerometer) to emulators. The server is compatible with Cemu, Dolphin, Yuzu, Ryujinx, and any client supporting the cemuhook protocol.
File: PadForge.App/Services/DsuMotionServer.cs
Namespace: PadForge.Services
Protocol Spec: https://github.com/v1993/cemuhook-protocol
public struct MotionSnapshot
{
public float AccelX, AccelY, AccelZ;
public float GyroPitch, GyroYaw, GyroRoll;
public long TimestampUs;
public bool HasMotion;
}Snapshot of a single slot's motion data, ready for DSU transmission. Units are already converted to DSU conventions:
- Accel: g-force (1g = 9.80665 m/s^2)
- Gyro: degrees per second
AccelX = -ax (inverted)
AccelY = -ay (inverted)
AccelZ = -az (inverted)
GyroPitch = -gx (inverted)
GyroYaw = gy (not inverted)
GyroRoll = -gz (inverted)
DS4 uses inverted signs versus SDL's standard coordinate system. This mapping was derived from Switch Pro Controller's known-working BetterJoy-to-DSU mapping, translated through SDL standard coordinates. Verified with DualSense: all axes match DS4/DSU convention perfectly across both DualSense and Switch 2 Pro Controller.
Key insight: Accel and gyro must be in the same coordinate frame. AccelX and GyroPitch must reference the same physical axis.
public sealed class DsuMotionServer : IDisposable| Constant | Value | Description |
|---|---|---|
MaxSlots |
4 |
DSU protocol slot limit (slots 4-7 skip DSU broadcast) |
ProtocolVersion |
1001 |
cemuhook protocol version |
HeaderSize |
16 |
Packet header size in bytes |
MsgTypeVersion |
0x100000 |
Server -> client version response |
MsgTypeControllerInfo |
0x100001 |
Server -> client controller info |
MsgTypePadData |
0x100002 |
Server -> client pad data |
ClientTimeoutMs |
5000 |
Client subscription expiry (5 seconds) |
SIO_UDP_CONNRESET |
0x9800000C |
IOControl to suppress ICMP port-unreachable resets |
| Field | Type | Description |
|---|---|---|
_socket |
Socket |
UDP socket bound to loopback |
_receiveThread |
Thread |
Background receive loop thread |
_running |
volatile bool |
Server running flag |
_serverId |
uint |
Unique server ID (from Environment.TickCount) |
_port |
int |
Listening port |
_packetCounters |
uint[4] |
Per-slot packet counter for protocol |
_subscriptions |
Dictionary<(EndPoint, int), long> |
Per-slot client subscriptions with timestamp |
_allSlotSubscriptions |
Dictionary<EndPoint, long> |
All-slot client subscriptions |
_slotConnected |
bool[4] |
Per-slot connection state |
_slotHasMotion |
bool[4] |
Per-slot motion capability |
public event EventHandler<string> StatusChanged;Raised when server status changes. Values: "Listening on :{port}", "Port {port} in use", "Failed to start", "Stopped".
public bool Start(int port = 26760)- Creates UDP socket on
IPAddress.Loopback. - Applies
SIO_UDP_CONNRESETIOControl to suppress ICMP port-unreachable exceptions on Windows. - Binds to port and starts background receive thread (
PadForge.DsuServer). - Returns
falseif port is already in use (SocketError.AddressAlreadyInUse).
public void Stop()Sets _running = false, closes socket, joins receive thread (2s timeout), clears subscriptions and packet counters.
public void BroadcastMotion(int slot, MotionSnapshot snapshot, bool connected)Called from the InputManager polling thread at ~1000Hz. Updates slot state and broadcasts motion data to all subscribed clients for the given slot. Only builds and sends packets if there are active subscribers (via GetSubscribers()).
Offset Size Field
[0..3] 4 Magic: "DSUS" (server->client) or "DSUC" (client->server)
[4..5] 2 Protocol version (1001)
[6..7] 2 Payload length (excluding header)
[8..11] 4 CRC32 (zeroed before computation)
[12..15] 4 Server ID (server->client) or Client ID (client->server)
private void WriteHeader(byte[] packet, int payloadLength, uint msgType)Message type: 0x100000
No additional payload beyond the message type.
Offset (payload) Size Field
[+0..3] 4 Message type (0x100000)
[+4..5] 2 Protocol version (1001)
[+6..7] 2 Padding (zero)
Total payload: 8 bytes.
Offset (payload) Size Field
[+0..3] 4 Message type (0x100001)
[+4..7] 4 Number of ports requested
[+8..N] N Slot indices (one byte each)
Offset (payload) Size Field
[+0..3] 4 Message type (0x100001)
[+4] 1 Slot number
[+5] 1 Slot state (0=not connected, 2=connected)
[+6] 1 Device model (0=N/A, 2=full gyro)
[+7] 1 Connection type (0=N/A)
[+8..13] 6 MAC address (fake, unique per slot: 00:00:00:00:00:{slot})
[+14] 1 Battery status (0x05 = charged)
[+15] 1 Padding
Total payload: 16 bytes.
Offset (payload) Size Field
[+0..3] 4 Message type (0x100002)
[+4] 1 Flags (0=all pads, 0x01=by slot ID, 0x02=by MAC)
[+5] 1 Slot number
[+6..11] 6 MAC address
The largest message type. Contains controller info header, button state, analog inputs, touch data, and motion sensor data.
Offset (payload) Size Field
[+0..3] 4 Message type (0x100002)
[+4] 1 Slot number
[+5] 1 Slot state (0=disconnected, 2=connected)
[+6] 1 Device model (0=N/A, 2=full gyro)
[+7] 1 Connection type (0=N/A)
[+8..13] 6 MAC address
[+14] 1 Battery status (0x05)
[+15] 1 Connected flag (1=connected)
[+16..19] 4 Packet counter (uint32, incremented per packet)
[+20] 1 Buttons bitmask 1 (DPad + Options + L3/R3 + Share)
[+21] 1 Buttons bitmask 2 (face + shoulders + triggers)
[+22] 1 Home button
[+23] 1 Touch button
[+24] 1 Left stick X (0-255, 128=center)
[+25] 1 Left stick Y (0-255, 128=center)
[+26] 1 Right stick X
[+27] 1 Right stick Y
[+28..31] 4 Analog D-Pad (left, down, right, up)
[+32..39] 8 Analog buttons
[+40..45] 6 Touch 1 data (active, id, x16, y16)
[+46..51] 6 Touch 2 data
[+52..59] 8 Motion timestamp (uint64, microseconds)
[+60..63] 4 Accel X (float, little-endian)
[+64..67] 4 Accel Y (float)
[+68..71] 4 Accel Z (float)
[+72..75] 4 Gyro Pitch (float)
[+76..79] 4 Gyro Yaw (float)
[+80..83] 4 Gyro Roll (float)
Total payload: 84 bytes (4 bytes message type + 80 bytes data).
Note: PadForge is a motion-only server. Button bitmasks, analog sticks, D-pad, analog buttons, and touch data are all zeroed (sticks centered at 128). Only the motion timestamp and accel/gyro fields carry real data.
private List<EndPoint> GetSubscribers(int slot)Returns a list of endpoints subscribed to the given slot. Maintains two subscription dictionaries:
-
Per-slot subscriptions (
_subscriptions): Keyed by(EndPoint, slotIndex). -
All-slot subscriptions (
_allSlotSubscriptions): Keyed byEndPoint.
Subscription flags in pad data requests:
-
flags == 0: Subscribe to all pads. -
flags & 0x01: Subscribe to specific slot by ID. -
flags & 0x02: Subscribe by MAC (treated as all-slot).
Subscriptions expire after ClientTimeoutMs (5 seconds). GetSubscribers() prunes expired entries during iteration using Stopwatch.GetTimestamp() for high-resolution timing.
All subscription access is protected by lock(_subscriptions).
private static uint ComputeCrc32(byte[] data, int length)
private static void FinalizeCrc(byte[] packet)Standard CRC32 with polynomial 0xEDB88320. Lookup table generated at static init via GenerateCrc32Table().
To compute: zero the CRC field [8..11], compute CRC over the entire packet, write result back to [8..11].
To verify: read CRC from [8..11], zero the field, compute, compare.
private void ReceiveLoop()Background thread (IsBackground = true, name "PadForge.DsuServer"). Loops ReceiveFrom() until _running is false. Each received packet is validated:
- Check magic bytes:
"DSUC"(client -> server). - Check protocol version (must be <= 1001).
- Validate payload length against actual received bytes.
- Verify CRC32.
- Dispatch based on message type to
HandleVersionRequest,HandleControllerInfoRequest, orHandlePadDataRequest.
Exceptions during receive are caught silently (SocketException when !_running means shutdown; ObjectDisposedException means socket closed; all others are malformed packets).
- Receive thread: Background thread reading from UDP socket. Handles client requests and manages subscriptions.
-
Polling thread: InputManager thread calls
BroadcastMotion()at ~1000Hz. Builds and sends pad data packets directly (no queue). -
Synchronization:
_subscriptionsdictionary protected bylock._runningisvolatile bool. Packet counters accessed only from the broadcast path (single caller per slot).
The DSU protocol supports a maximum of 4 slots. PadForge supports up to 8 virtual controller slots, but only slots 0-3 participate in DSU broadcasts. Slots 4-7 skip DSU entirely.
Location: tools/DsuDiag/
Standalone DSU client diagnostic tool that connects to the DSU server and displays received motion data per-slot in real-time. Used for debugging axis mapping and verifying protocol compliance.