From ce4a30bff1b7f9883a17e448431d52bf66c4117f Mon Sep 17 00:00:00 2001 From: Harry Reblando Date: Thu, 23 Jul 2026 16:06:21 -0700 Subject: [PATCH] Add DualSense helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80d5afef-9271-4daa-9d51-e464d824a3ac --- README.md | 4 + companion/DualSense/DualSenseHelper.h | 318 ++++++++++++++++++++++++++ companion/DualSense/README.md | 41 ++++ 3 files changed, 363 insertions(+) create mode 100644 companion/DualSense/DualSenseHelper.h create mode 100644 companion/DualSense/README.md diff --git a/README.md b/README.md index 73b7808..8aa8d53 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ GameInput is a next-generation input API that exposes input devices of all kinds GameInput was designed with ease of use as a top priority. Most common input tasks can be implemented with just a few lines of code. +## Companion helpers + +* [DualSense helper](companion/DualSense/README.md) — header-only helper for sending raw DualSense output reports through GameInput. + ## Version History ### Version 3.3 diff --git a/companion/DualSense/DualSenseHelper.h b/companion/DualSense/DualSenseHelper.h new file mode 100644 index 0000000..bd35c3b --- /dev/null +++ b/companion/DualSense/DualSenseHelper.h @@ -0,0 +1,318 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// This file is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// DualSense helper (header-only) for controlling lightbar, LEDs, +// rumble, and adaptive triggers via GameInput raw output reports. +// +// Requires GameInput version 3 or later (IGameInputDevice::CreateRawDeviceReport, +// IGameInputDevice::SendRawDeviceOutput, IGameInputRawDeviceReport). +// +// Usage: +// using namespace GameInput::DualSense; +// if (IsDualSense(info->vendorId, info->productId)) +// { +// SetDualSenseRumble(device, 255, 96); +// SetDualSenseLightbar(device, 255, 0, 0); +// SetDualSenseTriggerFeedback(device, Trigger::Right, 0x90, 0xFF); +// } + +#pragma once + +#include + +#if !defined(GAMEINPUT_API_VERSION) || GAMEINPUT_API_VERSION < 3 +#error DualSenseHelper requires GameInput API version 3 or later. +#endif + +#include +#include + +namespace GameInput { namespace DualSense { + +enum class Trigger +{ + Right, + Left, +}; + +enum class PlayerLed : uint8_t +{ + Off = 0, + Player1 = 1, + Player2 = 2, + Player3 = 3, + Player4 = 4, +}; + +namespace Detail { + + constexpr uint32_t ReportSize = 64; + constexpr uint8_t UsbOutputReportId = 0x02; + constexpr uint16_t SonyVendorId = 0x054C; + constexpr uint16_t DualSenseProductId = 0x0CE6; + constexpr uint16_t DualSenseEdgeProductId = 0x0DF2; + + // flag0 (byte 1): enables rumble/haptic/trigger features + constexpr uint32_t Flag0Offset = 1; + constexpr uint8_t Flag0Vibration = 0x01; + constexpr uint8_t Flag0HapticFilter = 0x02; + constexpr uint8_t Flag0RightTrigger = 0x04; + constexpr uint8_t Flag0LeftTrigger = 0x08; + + // flag1 (byte 2): selects which features to apply + constexpr uint32_t Flag1Offset = 2; + constexpr uint8_t Flag1MicControl = 0x01; + constexpr uint8_t Flag1Lightbar = 0x04; + constexpr uint8_t Flag1PlayerLed = 0x10; + + // Motor bytes + constexpr uint32_t MotorRightOffset = 3; + constexpr uint32_t MotorLeftOffset = 4; + + // Mute LED / power save + constexpr uint32_t MuteLedOffset = 9; + constexpr uint32_t PowerSaveOffset = 10; + + // Adaptive trigger regions + constexpr uint32_t TriggerEffectSize = 8; + constexpr uint32_t TriggerBlockStride = 11; + constexpr uint32_t RightTriggerOffset = 0x0B; + constexpr uint32_t LeftTriggerOffset = RightTriggerOffset + TriggerBlockStride; + + // flag2: secondary feature control + constexpr uint32_t Flag2Offset = 40; + constexpr uint8_t Flag2LightbarSetup = 0x02; + + // Lightbar setup & LED + constexpr uint32_t LightbarSetupOffset = 42; + constexpr uint8_t LightbarFadeOut = 0x02; + constexpr uint32_t PlayerLedOffset = 44; + + // Lightbar RGB + constexpr uint32_t LightbarRedOffset = 45; + constexpr uint32_t LightbarGreenOffset = 46; + constexpr uint32_t LightbarBlueOffset = 47; + + // Player LED bit patterns (5 LEDs across the touchpad) + constexpr uint8_t PlayerLedPatterns[] = { 0x00, 0x04, 0x0A, 0x15, 0x1B }; + + inline uint8_t GetTriggerFlag(Trigger trigger) noexcept + { + return (trigger == Trigger::Right) ? Flag0RightTrigger : Flag0LeftTrigger; + } + + inline uint32_t GetTriggerOffset(Trigger trigger) noexcept + { + return (trigger == Trigger::Right) ? RightTriggerOffset : LeftTriggerOffset; + } + + inline void ClearTriggerBlock(uint8_t* buf, uint32_t offset) noexcept + { + memset(&buf[offset], 0, TriggerEffectSize); + } + + inline void EnableTriggerEffect(uint8_t* buf, Trigger trigger) noexcept + { + buf[Flag0Offset] |= GetTriggerFlag(trigger); + } + + inline HRESULT SendReport(GameInput::v3::IGameInputDevice* device, const uint8_t* buf) noexcept + { + if (device == nullptr) + { + return E_POINTER; + } + + GameInput::v3::IGameInputRawDeviceReport* rawReport = nullptr; + HRESULT hr = device->CreateRawDeviceReport( + buf[0], + GameInput::v3::GameInputRawOutputReport, + &rawReport); + if (FAILED(hr)) + { + return hr; + } + + if (!rawReport->SetRawData(ReportSize, buf)) + { + rawReport->Release(); + return E_INVALIDARG; + } + + hr = device->SendRawDeviceOutput(rawReport); + rawReport->Release(); + return hr; + } + +} // namespace Detail + +// Controller identification + +inline bool IsDualSense(uint16_t vendorId, uint16_t productId) noexcept +{ + return vendorId == Detail::SonyVendorId && + (productId == Detail::DualSenseProductId || productId == Detail::DualSenseEdgeProductId); +} + +// Lightbar / LEDs / Rumble + +// Set lightbar color (RGB, each 0-255). +inline HRESULT SetDualSenseLightbar( + GameInput::v3::IGameInputDevice* device, + uint8_t r, + uint8_t g, + uint8_t b) noexcept +{ + uint8_t buf[Detail::ReportSize] = {Detail::UsbOutputReportId}; + buf[Detail::Flag1Offset] |= Detail::Flag1Lightbar; + buf[Detail::LightbarRedOffset] = r; + buf[Detail::LightbarGreenOffset] = g; + buf[Detail::LightbarBlueOffset] = b; + return Detail::SendReport(device, buf); +} + +// Turn off the lightbar (fade out). +inline HRESULT TurnOffDualSenseLightbar(GameInput::v3::IGameInputDevice* device) noexcept +{ + uint8_t buf[Detail::ReportSize] = {Detail::UsbOutputReportId}; + buf[Detail::Flag1Offset] |= Detail::Flag1Lightbar; + buf[Detail::Flag2Offset] |= Detail::Flag2LightbarSetup; + buf[Detail::LightbarSetupOffset] = Detail::LightbarFadeOut; + return Detail::SendReport(device, buf); +} + +// Set player LED indicator. +inline HRESULT SetDualSensePlayerLed( + GameInput::v3::IGameInputDevice* device, + PlayerLed player) noexcept +{ + uint8_t buf[Detail::ReportSize] = {Detail::UsbOutputReportId}; + buf[Detail::Flag1Offset] |= Detail::Flag1PlayerLed; + const uint8_t index = static_cast(player); + buf[Detail::PlayerLedOffset] = (index <= 4) ? Detail::PlayerLedPatterns[index] : 0; + return Detail::SendReport(device, buf); +} + +// Set rumble motor intensities (each 0-255). +// leftMotor = strong/low-frequency, rightMotor = weak/high-frequency. +inline HRESULT SetDualSenseRumble( + GameInput::v3::IGameInputDevice* device, + uint8_t leftMotor, + uint8_t rightMotor) noexcept +{ + uint8_t buf[Detail::ReportSize] = {Detail::UsbOutputReportId}; + buf[Detail::Flag0Offset] |= Detail::Flag0Vibration | Detail::Flag0HapticFilter; + buf[Detail::MotorLeftOffset] = leftMotor; + buf[Detail::MotorRightOffset] = rightMotor; + return Detail::SendReport(device, buf); +} + +// Set the microphone mute LED. +inline HRESULT SetDualSenseMuteLed(GameInput::v3::IGameInputDevice* device, bool on) noexcept +{ + uint8_t buf[Detail::ReportSize] = {Detail::UsbOutputReportId}; + buf[Detail::Flag1Offset] |= Detail::Flag1MicControl; + buf[Detail::MuteLedOffset] = on ? 0x01 : 0x00; + buf[Detail::PowerSaveOffset] = on ? 0x00 : 0x10; + return Detail::SendReport(device, buf); +} + +// Adaptive Trigger Effects +// +// Only three mode flags are known to work on real hardware. +// All trigger params are raw uint8_t bytes matching the HID report layout. + +// Off: clears any trigger effect. +inline HRESULT SetDualSenseTriggerOff( + GameInput::v3::IGameInputDevice* device, + Trigger trigger) noexcept +{ + uint8_t buf[Detail::ReportSize] = {Detail::UsbOutputReportId}; + Detail::EnableTriggerEffect(buf, trigger); + Detail::ClearTriggerBlock(buf, Detail::GetTriggerOffset(trigger)); + return Detail::SendReport(device, buf); +} + +// Feedback: no resistance until a starting point, then constant force. +// startPosition: where resistance begins (0 = top, 255 = bottom). +// force: amount of feedback after the start point. +inline HRESULT SetDualSenseTriggerFeedback( + GameInput::v3::IGameInputDevice* device, + Trigger trigger, + uint8_t startPosition, + uint8_t force) noexcept +{ + uint8_t buf[Detail::ReportSize] = {Detail::UsbOutputReportId}; + const uint32_t offset = Detail::GetTriggerOffset(trigger); + Detail::EnableTriggerEffect(buf, trigger); + Detail::ClearTriggerBlock(buf, offset); + buf[offset + 0] = 0x01; + buf[offset + 1] = startPosition; + buf[offset + 2] = force; + return Detail::SendReport(device, buf); +} + +// Weapon: resistance region with a breakout force. +// startPosition: where resistance begins (0 = top, 255 = bottom). +// endPosition: where resistance ends (0 = top, 255 = bottom). +// strength: how strong the resistance is to break through. +inline HRESULT SetDualSenseTriggerWeapon( + GameInput::v3::IGameInputDevice* device, + Trigger trigger, + uint8_t startPosition, + uint8_t endPosition, + uint8_t strength) noexcept +{ + uint8_t buf[Detail::ReportSize] = {Detail::UsbOutputReportId}; + const uint32_t offset = Detail::GetTriggerOffset(trigger); + Detail::EnableTriggerEffect(buf, trigger); + Detail::ClearTriggerBlock(buf, offset); + buf[offset + 0] = 0x02; + buf[offset + 1] = startPosition; + buf[offset + 2] = endPosition; + buf[offset + 3] = strength; + return Detail::SendReport(device, buf); +} + +// Vibration: vibrates the trigger with configurable frequency and resistance. +// frequency: vibration speed (0 = low, 255 = high). +// resistance: resistance level (0 = high, 255 = low / inverted). +// startPosition: where vibration + resistance begin (0 = top, 147 = bottom). +inline HRESULT SetDualSenseTriggerVibration( + GameInput::v3::IGameInputDevice* device, + Trigger trigger, + uint8_t frequency, + uint8_t resistance, + uint8_t startPosition) noexcept +{ + uint8_t buf[Detail::ReportSize] = {Detail::UsbOutputReportId}; + const uint32_t offset = Detail::GetTriggerOffset(trigger); + Detail::EnableTriggerEffect(buf, trigger); + Detail::ClearTriggerBlock(buf, offset); + buf[offset + 0] = 0x06; + buf[offset + 1] = frequency; + buf[offset + 2] = resistance; + buf[offset + 3] = startPosition; + return Detail::SendReport(device, buf); +} + +}} // namespace GameInput::DualSense diff --git a/companion/DualSense/README.md b/companion/DualSense/README.md new file mode 100644 index 0000000..505f4df --- /dev/null +++ b/companion/DualSense/README.md @@ -0,0 +1,41 @@ +# DualSense GameInput helper + +`DualSenseHelper.h` is a header-only sample for sending raw output reports through GameInput. It identifies supported controllers and provides helpers for lightbar color and fade-out, player LEDs, rumble, the microphone mute LED, and adaptive trigger off, feedback, weapon, and vibration effects. + +## Requirements and supported devices + +* GameInput API version 3 or later is required for raw output report creation and sending. +* Sony vendor ID `0x054C`, DualSense product ID `0x0CE6`. +* Sony vendor ID `0x054C`, DualSense Edge product ID `0x0DF2`. + +Device firmware and transport behavior can vary. Check every returned `HRESULT`. + +## Include and use + +Add this directory to your include path or copy `DualSenseHelper.h` into your project, then include it directly. The header includes `` and has no other project dependencies. + +```cpp +#include "DualSenseHelper.h" + +const GameInput::v3::GameInputDeviceInfo* info = nullptr; +HRESULT hr = device->GetDeviceInfo(&info); +if (SUCCEEDED(hr) && + GameInput::DualSense::IsDualSense(info->vendorId, info->productId)) +{ + hr = GameInput::DualSense::SetDualSenseLightbar(device, 255, 0, 0); + if (SUCCEEDED(hr)) + { + hr = GameInput::DualSense::SetDualSenseRumble(device, 255, 96); + } +} +``` + +Obtain `device` as a `GameInput::v3::IGameInputDevice*` through normal GameInput device discovery. Color, motor, force, position, frequency, resistance, and strength parameters are raw byte values from 0 through 255, not normalized floating-point values. The trigger-vibration start position uses 0 through 147 as its documented physical range. + +## Raw report limitations + +The helper constructs a 64-byte USB-format output report whose first byte is report ID `0x02`. It does not implement Bluetooth framing, checksums, transport selection, input parsing, or connection management, and makes no claim of Bluetooth support. Raw HID report behavior is device- and firmware-specific; this sample is not an official Sony SDK and does not imply support from Sony Interactive Entertainment. + +"PlayStation" is a registered trademark or trademark of Sony Interactive Entertainment Inc. + +"DualSense" is a registered trademark or trademark of Sony Interactive Entertainment Inc.