SDL: Added support for the new Steam Controller

From 1998b650452bdf0bee5209e20e4715b4295abe8c Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 12 Nov 2025 11:32:32 -0800
Subject: [PATCH] Added support for the new Steam Controller

---
 VisualC/SDL/SDL.vcxproj                       |   9 +-
 VisualC/SDL/SDL.vcxproj.filters               |   4 +
 Xcode/SDL/SDL.xcodeproj/project.pbxproj       |   4 +
 src/hidapi/ios/hid.m                          | 283 +++++++---
 src/joystick/SDL_gamepad.c                    |   3 +
 src/joystick/SDL_joystick.c                   |   6 +
 src/joystick/SDL_joystick_c.h                 |   3 +
 src/joystick/controller_list.h                |  10 +-
 src/joystick/controller_type.h                |   2 +
 src/joystick/hidapi/SDL_hidapi_steam_triton.c | 532 ++++++++++++++++++
 src/joystick/hidapi/SDL_hidapijoystick.c      |   3 +
 src/joystick/hidapi/SDL_hidapijoystick_c.h    |   2 +
 .../hidapi/steam/controller_constants.h       |   8 +-
 .../hidapi/steam/controller_structs.h         | 227 ++++++--
 src/joystick/usb_ids.h                        |   2 +
 15 files changed, 971 insertions(+), 127 deletions(-)
 create mode 100644 src/joystick/hidapi/SDL_hidapi_steam_triton.c

diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index 7af7132b29a66..f5a1612f4dee0 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -82,16 +82,16 @@
     <LibraryPath Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">C:\Program Files %28x86%29\Microsoft DirectX SDK %28June 2010%29\Lib\x86;$(LibraryPath)</LibraryPath>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
-    <IncludePath>$(ProjectDir)/../../src;$(IncludePath)</IncludePath>
+    <IncludePath>$(ProjectDir)/../../src;$(ProjectDir)/../../src/core/windows;$(IncludePath)</IncludePath>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
-    <IncludePath>$(ProjectDir)/../../src;$(IncludePath)</IncludePath>
+    <IncludePath>$(ProjectDir)/../../src;$(ProjectDir)/../../src/core/windows;$(IncludePath)</IncludePath>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
-    <IncludePath>$(ProjectDir)/../../src;$(IncludePath)</IncludePath>
+    <IncludePath>$(ProjectDir)/../../src;$(ProjectDir)/../../src/core/windows;$(IncludePath)</IncludePath>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
-    <IncludePath>$(ProjectDir)/../../src;$(IncludePath)</IncludePath>
+    <IncludePath>$(ProjectDir)/../../src;$(ProjectDir)/../../src/core/windows;$(IncludePath)</IncludePath>
   </PropertyGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
     <PreBuildEvent>
@@ -625,6 +625,7 @@
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_stadia.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_steam.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_steam_hori.c" />
+    <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_steam_triton.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_steamdeck.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_switch.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_switch2.c" />
diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters
index 77bd59d5fb9a3..7e5aa144c717c 100644
--- a/VisualC/SDL/SDL.vcxproj.filters
+++ b/VisualC/SDL/SDL.vcxproj.filters
@@ -1660,6 +1660,10 @@
     <ClCompile Include="..\..\src\storage\generic\SDL_genericstorage.c" />
     <ClCompile Include="..\..\src\storage\steam\SDL_steamstorage.c" />
     <ClCompile Include="..\..\src\storage\SDL_storage.c" />
+    <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_steam_triton.c">
+      <Filter>joystick\hidapi</Filter>
+    </ClCompile>
+  </ItemGroup>
     <ClCompile Include="..\..\src\events\SDL_eventwatch.c" />
     <ClCompile Include="..\..\src\core\windows\pch_cpp.cpp">
       <Filter>core\windows</Filter>
diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj
index bd3c41722cc05..6d57ae7e17250 100644
--- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj
+++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj
@@ -415,6 +415,7 @@
 		F386F6F92884663E001840AA /* SDL_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = F386F6E62884663E001840AA /* SDL_utils.c */; };
 		F388C95528B5F6F700661ECF /* SDL_hidapi_ps3.c in Sources */ = {isa = PBXBuildFile; fileRef = F388C95428B5F6F600661ECF /* SDL_hidapi_ps3.c */; };
 		F39344CE2E99771B0056986F /* SDL_dlopennote.h in Headers */ = {isa = PBXBuildFile; fileRef = F39344CD2E99771B0056986F /* SDL_dlopennote.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		F38C72492CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c in Sources */ = {isa = PBXBuildFile; fileRef = F38C72482CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c */; };
 		F395BF6525633B2400942BFF /* SDL_crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = F395BF6425633B2400942BFF /* SDL_crc32.c */; };
 		F395C1932569C68F00942BFF /* SDL_iokitjoystick_c.h in Headers */ = {isa = PBXBuildFile; fileRef = F395C1912569C68E00942BFF /* SDL_iokitjoystick_c.h */; };
 		F395C19C2569C68F00942BFF /* SDL_iokitjoystick.c in Sources */ = {isa = PBXBuildFile; fileRef = F395C1922569C68E00942BFF /* SDL_iokitjoystick.c */; };
@@ -998,6 +999,7 @@
 		F386F6E62884663E001840AA /* SDL_utils.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_utils.c; sourceTree = "<group>"; };
 		F388C95428B5F6F600661ECF /* SDL_hidapi_ps3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_ps3.c; sourceTree = "<group>"; };
 		F39344CD2E99771B0056986F /* SDL_dlopennote.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_dlopennote.h; sourceTree = "<group>"; };
+		F38C72482CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_steam_triton.c; sourceTree = "<group>"; };
 		F395BF6425633B2400942BFF /* SDL_crc32.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_crc32.c; sourceTree = "<group>"; };
 		F395C1912569C68E00942BFF /* SDL_iokitjoystick_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_iokitjoystick_c.h; sourceTree = "<group>"; };
 		F395C1922569C68E00942BFF /* SDL_iokitjoystick.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_iokitjoystick.c; sourceTree = "<group>"; };
@@ -1962,6 +1964,7 @@
 				F3984CCF25BCC92800374F43 /* SDL_hidapi_stadia.c */,
 				A75FDAAC23E2795C00529352 /* SDL_hidapi_steam.c */,
 				F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */,
+				F38C72482CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c */,
 				A797456F2B2E9D39009D224A /* SDL_hidapi_steamdeck.c */,
 				A7D8A7C623E2513E00DCD162 /* SDL_hidapi_switch.c */,
 				A7D8A7C623E2513E00DCD163 /* SDL_hidapi_switch2.c */,
@@ -3009,6 +3012,7 @@
 				F316ABD92B5C3185002EF551 /* SDL_memcpy.c in Sources */,
 				A7D8B97A23E2514400DCD162 /* SDL_render.c in Sources */,
 				A7D8ABD323E2514100DCD162 /* SDL_stretch.c in Sources */,
+				F38C72492CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c in Sources */,
 				A7D8AC3923E2514100DCD162 /* SDL_blit_copy.c in Sources */,
 				A7D8B5CF23E2514300DCD162 /* SDL_syspower.m in Sources */,
 				F3B439512C935C2400792030 /* SDL_dummyprocess.c in Sources */,
diff --git a/src/hidapi/ios/hid.m b/src/hidapi/ios/hid.m
index cb5e2317599ad..24a3c791ee1d7 100644
--- a/src/hidapi/ios/hid.m
+++ b/src/hidapi/ios/hid.m
@@ -63,6 +63,7 @@
 
 #define VALVE_USB_VID       0x28DE
 #define D0G_BLE2_PID        0x1106
+#define TRITON_BLE_PID	    0x1303
 
 typedef uint32_t uint32;
 typedef uint64_t uint64;
@@ -76,7 +77,8 @@
 #define VALVE_SERVICE		@"100F6C32-1735-4313-B402-38567131E5F3"
 
 // (READ/NOTIFICATIONS)
-#define VALVE_INPUT_CHAR	@"100F6C33-1735-4313-B402-38567131E5F3"
+#define VALVE_INPUT_CHAR_0x1106	@"100F6C33-1735-4313-B402-38567131E5F3"
+#define VALVE_INPUT_CHAR_0x1303	@"100F6C77-1735-4313-B402-38567131E5F3"
 
 //  (READ/WRITE)
 #define VALVE_REPORT_CHAR	@"100F6C34-1735-4313-B402-38567131E5F3"
@@ -101,21 +103,7 @@
 
 typedef struct {
 	uint8_t		id;
-	union {
-		bluetoothSegment segment;
-		struct {
-			uint8_t		segmentHeader;
-			uint8_t		featureReportMessageID;
-			uint8_t		length;
-			uint8_t		settingIdentifier;
-			union {
-				uint16_t	usPayload;
-				uint32_t	uPayload;
-				uint64_t	ulPayload;
-				uint8_t		ucPayload[15];
-			};
-		};
-	};
+	bluetoothSegment segment;
 } hidFeatureReport;
 
 #pragma pack(pop)
@@ -125,34 +113,62 @@ size_t GetBluetoothSegmentSize(bluetoothSegment *segment)
     return segment->length + 3;
 }
 
-#define RingBuffer_cbElem   19
-#define RingBuffer_nElem    4096
+#define RingBuffer_nElem    256
 
 typedef struct {
 	int _first, _last;
-	uint8_t _data[ ( RingBuffer_nElem * RingBuffer_cbElem ) ];
+	int _cbElem;
+	uint8_t *_data;
 	pthread_mutex_t accessLock;
 } RingBuffer;
 
-static void RingBuffer_init( RingBuffer *this )
+static RingBuffer *RingBuffer_alloc( int cbElem )
 {
+    RingBuffer *this = (RingBuffer *)malloc( sizeof(*this) );
+    if (!this)
+{
+        return NULL;
+    }
+
     this->_first = -1;
     this->_last = 0;
+    this->_cbElem = cbElem;
+    this->_data = (uint8_t *)malloc(RingBuffer_nElem * cbElem);
+    if ( !this->_data )
+    {
+        free( this );
+        return NULL;
+    }
     pthread_mutex_init( &this->accessLock, 0 );
+    return this;
+}
+
+static void RingBuffer_free( RingBuffer *this )
+{
+    if ( this )
+    {
+        free( this->_data );
+        free( this );
+    }
 }
 
 static bool RingBuffer_write( RingBuffer *this, const uint8_t *src )
 {
+    if ( !this )
+    {
+        return false;
+    }
+
     pthread_mutex_lock( &this->accessLock );
-    memcpy( &this->_data[ this->_last ], src, RingBuffer_cbElem );
+    memcpy( &this->_data[ this->_last ], src, this->_cbElem );
     if ( this->_first == -1 )
     {
         this->_first = this->_last;
     }
-    this->_last = ( this->_last + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem);
+    this->_last = ( this->_last + this->_cbElem ) % (RingBuffer_nElem * this->_cbElem);
     if ( this->_last == this->_first )
     {
-        this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem);
+        this->_first = ( this->_first + this->_cbElem ) % (RingBuffer_nElem * this->_cbElem);
         pthread_mutex_unlock( &this->accessLock );
         return false;
     }
@@ -162,14 +178,19 @@ static bool RingBuffer_write( RingBuffer *this, const uint8_t *src )
 
 static bool RingBuffer_read( RingBuffer *this, uint8_t *dst )
 {
+    if ( !this )
+    {
+        return false;
+    }
+
     pthread_mutex_lock( &this->accessLock );
     if ( this->_first == -1 )
     {
         pthread_mutex_unlock( &this->accessLock );
         return false;
     }
-    memcpy( dst, &this->_data[ this->_first ], RingBuffer_cbElem );
-    this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem);
+    memcpy( dst, &this->_data[ this->_first ], this->_cbElem );
+    this->_first = ( this->_first + this->_cbElem ) % (RingBuffer_nElem * this->_cbElem);
     if ( this->_first == this->_last )
     {
         this->_first = -1;
@@ -191,12 +212,14 @@ static bool RingBuffer_read( RingBuffer *this, uint8_t *dst )
 
 @interface HIDBLEDevice : NSObject <CBPeripheralDelegate>
 {
-	RingBuffer _inputReports;
-	uint8_t	_featureReport[20];
+	RingBuffer *_inputReports;
+    NSData *_featureReport;
+	NSMutableDictionary *_outputReports;
 	BLEDeviceWaitState	_waitStateForReadFeatureReport;
 	BLEDeviceWaitState	_waitStateForWriteFeatureReport;
 }
 
+@property (nonatomic, readwrite) uint16_t pid;
 @property (nonatomic, readwrite) bool connected;
 @property (nonatomic, readwrite) bool ready;
 
@@ -205,6 +228,7 @@ @interface HIDBLEDevice : NSObject <CBPeripheralDelegate>
 @property (nonatomic, strong) CBCharacteristic *bleCharacteristicReport;
 
 - (id)initWithPeripheral:(CBPeripheral *)peripheral;
+- (void)onDisconnect;
 
 @end
 
@@ -278,8 +302,7 @@ - (void)appWillResignActiveNotification:(NSNotification *)note
 		HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral];
 		if ( steamController )
 		{
-			steamController.connected = NO;
-			steamController.ready = NO;
+            [steamController onDisconnect];
 			[self.centralManager cancelPeripheralConnection:peripheral];
 		}
 	}
@@ -474,8 +497,7 @@ - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPe
 	HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral];
 	if ( steamController )
 	{
-		steamController.connected = NO;
-		steamController.ready = NO;
+		[steamController onDisconnect];
 		[self.deviceMap removeObjectForKey:peripheral];
 	}
 }
@@ -500,12 +522,14 @@ - (id)init
 {
 	if ( self = [super init] )
 	{
-        RingBuffer_init( &_inputReports );
+		self.pid = 0;
+        _inputReports = NULL;
+		_outputReports = [[NSMutableDictionary alloc] init];
+		_connected = NO;
+		_ready = NO;
 		self.bleSteamController = nil;
 		self.bleCharacteristicInput = nil;
 		self.bleCharacteristicReport = nil;
-		_connected = NO;
-		_ready = NO;
 	}
 	return self;
 }
@@ -514,7 +538,9 @@ - (id)initWithPeripheral:(CBPeripheral *)peripheral
 {
 	if ( self = [super init] )
 	{
-        RingBuffer_init( &_inputReports );
+		self.pid = 0;
+        _inputReports = NULL;
+		_outputReports = [[NSMutableDictionary alloc] init];
 		_connected = NO;
 		_ready = NO;
 		self.bleSteamController = peripheral;
@@ -528,6 +554,18 @@ - (id)initWithPeripheral:(CBPeripheral *)peripheral
 	return self;
 }
 
+- (void)onDisconnect
+{
+    self.connected = NO;
+    self.ready = NO;
+
+    if ( _inputReports )
+    {
+        RingBuffer_free( _inputReports );
+        _inputReports = NULL;
+    }
+}
+
 - (void)setConnected:(bool)connected
 {
 	_connected = connected;
@@ -543,94 +581,134 @@ - (void)setConnected:(bool)connected
 
 - (size_t)read_input_report:(uint8_t *)dst
 {
-	if ( RingBuffer_read( &_inputReports, dst+1 ) )
+	if ( RingBuffer_read( _inputReports, dst+1 ) )
 	{
-		*dst = 0x03;
-		return 20;
+        switch ( self.pid )
+	{
+        case D0G_BLE2_PID:
+            *dst = 0x03;
+            break;
+        case TRITON_BLE_PID:
+            *dst = 0x42;
+            break;
+        default:
+            abort();
+        }
+		return _inputReports->_cbElem + 1;
 	}
 	return 0;
 }
 
 - (int)send_report:(const uint8_t *)data length:(size_t)length
 {
+	if ( self.pid == D0G_BLE2_PID )
+	{
 	[_bleSteamController writeValue:[NSData dataWithBytes:data length:length] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse];
 	return (int)length;
 }
 
-- (int)send_feature_report:(hidFeatureReport *)report
+	// We need to look up the correct characteristic for this output report
+	if ( length > 0 )
+	{
+		CBCharacteristic *aChar = [_outputReports objectForKey:[NSNumber numberWithInt:data[0]]];
+		if ( aChar != nil )
+		{
+			[_bleSteamController writeValue:[NSData dataWithBytes:&data[1] length:(length - 1)] forCharacteristic:aChar type:CBCharacteristicWriteWithResponse];
+			return (int)length;
+		}
+	}
+	return -1;
+}
+
+- (int)send_feature_report:(hidFeatureReport *)report length:(size_t)length
 {
 #if FEATURE_REPORT_LOGGING
 	uint8_t *reportBytes = (uint8_t *)report;
 
-	NSLog( @"HIDBLE:send_feature_report (%02zu/19) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]", GetBluetoothSegmentSize( report->segment ),
+	NSLog( @"HIDBLE:send_feature_report (%02zu/19) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]", length,
 		  reportBytes[1], reportBytes[2], reportBytes[3], reportBytes[4], reportBytes[5], reportBytes[6],
 		  reportBytes[7], reportBytes[8], reportBytes[9], reportBytes[10], reportBytes[11], reportBytes[12],
 		  reportBytes[13], reportBytes[14], reportBytes[15], reportBytes[16], reportBytes[17], reportBytes[18],
 		  reportBytes[19] );
 #endif
 
-	int sendSize = (int)GetBluetoothSegmentSize( &report->segment );
-	if ( sendSize > 20 )
-		sendSize = 20;
-
 #if 1
 	// fire-and-forget - we are going to not wait for the response here because all Steam Controller BLE send_feature_report's are ignored,
 	//  except errors.
-	[_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:sendSize] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse];
+	[_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:MIN(length, 64)] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse];
 
 	// pretend we received a result anybody cares about
-	return 19;
+	return (int)length;
 
 #else
 	// this is technically the correct send_feature_report logic if you want to make sure it gets through and is
 	// acknowledged or errors out
 	_waitStateForWriteFeatureReport = BLEDeviceWaitState_Waiting;
-	[_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:sendSize
+	[_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:MIN(length, 64)
 									 ] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse];
 
-	while ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Waiting )
+	while ( _connected && _waitStateForWriteFeatureReport == BLEDeviceWaitState_Waiting )
 	{
 		process_pending_events();
 	}
 
-	if ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Error )
+	if ( !_connected || _waitStateForWriteFeatureReport == BLEDeviceWaitState_Error )
 	{
 		_waitStateForWriteFeatureReport = BLEDeviceWaitState_None;
 		return -1;
 	}
 
 	_waitStateForWriteFeatureReport = BLEDeviceWaitState_None;
-	return 19;
+	return (int)length;
 #endif
 }
 
-- (int)get_feature_report:(uint8_t)feature into:(uint8_t *)buffer
+- (int)get_feature_report:(uint8_t)feature into:(uint8_t *)buffer length:(size_t)length
 {
 	_waitStateForReadFeatureReport = BLEDeviceWaitState_Waiting;
 	[_bleSteamController readValueForCharacteristic:_bleCharacteristicReport];
 
-	while ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Waiting )
-		process_pending_events();
+	while ( _connected && _waitStateForReadFeatureReport == BLEDeviceWaitState_Waiting )
+    {
+        process_pending_events();
+    }
 
-	if ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Error )
+	if ( !_connected || _waitStateForReadFeatureReport == BLEDeviceWaitState_Error )
 	{
 		_waitStateForReadFeatureReport = BLEDeviceWaitState_None;
 		return -1;
 	}
 
-	memcpy( buffer, _featureReport, sizeof(_featureReport) );
+    int amount = 0;
+    if ( _featureReport.length > 0 )
+    {
+        uint8_t *data = (uint8_t *)_featureReport.bytes;
+        if ( *data == *buffer )
+        {
+            amount = (int)MIN( length, _featureReport.length );
+            memcpy( buffer, _featureReport.bytes, amount );
+        }
+        else
+        {
+            // Leave the report in the buffer
+            amount = (int)MIN( length - 1, _featureReport.length );
+            memcpy( &buffer[ 1 ], _featureReport.bytes, amount );
+            ++amount;
+        }
+    }
 
 	_waitStateForReadFeatureReport = BLEDeviceWaitState_None;
 
 #if FEATURE_REPORT_LOGGING
-	NSLog( @"HIDBLE:get_feature_report (19) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]",
-		  buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6],
-		  buffer[7], buffer[8], buffer[9], buffer[10], buffer[11], buffer[12],
-		  buffer[13], buffer[14], buffer[15], buffer[16], buffer[17], buffer[18],
-		  buffer[19] );
+    NSLog( @"HIDBLE:get_feature_report (%lu/%zu) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]",
+      _featureReport.length, length,
+      buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6],
+      buffer[7], buffer[8], buffer[9], buffer[10], buffer[11], buffer[12],
+      buffer[13], buffer[14], buffer[15], buffer[16], buffer[17], buffer[18],
+      buffer[19] );
 #endif
 
-	return 19;
+	return amount;
 }
 
 #pragma mark CBPeripheralDelegate Implementation
@@ -667,8 +745,14 @@ - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForServi
 		{
 			NSLog( @"Found Characteristic %@", aChar );
 
-			if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR]] )
+			if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR_0x1106]] )
 			{
+				self.pid = D0G_BLE2_PID;
+				self.bleCharacteristicInput = aChar;
+			}
+			else if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR_0x1303]] )
+			{
+				self.pid = TRITON_BLE_PID;
 				self.bleCharacteristicInput = aChar;
 			}
 			else if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] )
@@ -676,6 +760,21 @@ - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForServi
 				self.bleCharacteristicReport = aChar;
 				[self.bleSteamController discoverDescriptorsForCharacteristic: aChar];
 			}
+			else
+			{
+                NSString *UUIDString = [aChar.UUID UUIDString];
+                int report_id = 0;
+                if ( sscanf( UUIDString.UTF8String, "100F6C%x", &report_id ) == 1 && report_id > 0x35 )
+                {
+                    report_id -= 0x35;
+                    //NSLog( @"Found characteristic for output report 0x%.2x", report_id );
+
+					if (report_id >= 0x80) {
+						// An output report
+                        [_outputReports setObject:aChar forKey:[NSNumber numberWithInt:report_id]];
+					}
+                }
+			}
 		}
 	}
 }
@@ -690,17 +789,33 @@ - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(C
 	if ( self.ready == NO )
 	{
 		self.ready = YES;
+        if ( _inputReports == NULL )
+        {
+            int cbElem = 0;
+            switch ( self.pid )
+            {
+            case D0G_BLE2_PID:
+                cbElem = 19;
+                break;
+            case TRITON_BLE_PID:
+                cbElem = 53;
+                break;
+            default:
+                abort();
+            }
+            _inputReports = RingBuffer_alloc( cbElem );
+        }
 		HIDBLEManager.sharedInstance.nPendingPairs -= 1;
 	}
 
 	if ( [characteristic.UUID isEqual:_bleCharacteristicInput.UUID] )
 	{
 		NSData *data = [characteristic value];
-		if ( data.length != 19 )
+		if ( _inputReports && data.length != _inputReports->_cbElem )
 		{
-			NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 19", (unsigned long)data.length );
+            NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly %d", (unsigned long)data.length, _inputReports->_cbElem );
 		}
-		if ( !RingBuffer_write( &_inputReports, (const uint8_t *)data.bytes ) )
+		if ( !RingBuffer_write( _inputReports, (const uint8_t *)data.bytes ) )
 		{
 			uint64_t ticksNow = mach_approximate_time();
 			if ( ticksNow - s_ticksLastOverflowReport > (5ull * NSEC_PER_SEC / 10) )
@@ -712,8 +827,6 @@ - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(C
 	}
 	else if ( [characteristic.UUID isEqual:_bleCharacteristicReport.UUID] )
 	{
-		memset( _featureReport, 0, sizeof(_featureReport) );
-
 		if ( error != nil )
 		{
 			NSLog( @"HIDBLE: get_feature_report error: %@", error );
@@ -721,12 +834,7 @@ - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(C
 		}
 		else
 		{
-			NSData *data = [characteristic value];
-			if ( data.length != 20 )
-			{
-				NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 20", (unsigned long)data.length );
-			}
-			memcpy( _featureReport, data.bytes, MIN( data.length, sizeof(_featureReport) ) );
+			_featureReport = [characteristic value];
 			_waitStateForReadFeatureReport = BLEDeviceWaitState_Complete;
 		}
 	}
@@ -850,7 +958,7 @@ int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock)
     memset( device_info, 0, sizeof(struct hid_device_info) );
     device_info->path = strdup( device.bleSteamController.identifier.UUIDString.UTF8String );
     device_info->vendor_id = VALVE_USB_VID;
-    device_info->product_id = D0G_BLE2_PID;
+    device_info->product_id = device.pid;
     device_info->product_string = wcsdup( L"Steam Controller" );
     device_info->manufacturer_string = wcsdup( L"Valve Corporation" );
     device_info->bus_type = HID_API_BUS_BLUETOOTH;
@@ -861,14 +969,6 @@ int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock)
 { @autoreleasepool {
 	struct hid_device_info *root = NULL;
 
-	/* See if there are any devices we should skip in enumeration */
-	if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_BLUETOOTH, VALVE_USB_VID, D0G_BLE2_PID, 0, 0)) {
-		return NULL;
-	}
-
-	if ( ( vendor_id == 0 || vendor_id == VALVE_USB_VID ) &&
-	     ( product_id == 0 || product_id == D0G_BLE2_PID ) )
-	{
 		HIDBLEManager *bleManager = HIDBLEManager.sharedInstance;
 		[bleManager updateConnectedSteamControllers:false];
 		NSEnumerator<HIDBLEDevice *> *devices = [bleManager.deviceMap objectEnumerator];
@@ -891,11 +991,22 @@ int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock)
 				}
 				continue;
 			}
+
+        if ( ( vendor_id != 0 && vendor_id != VALVE_USB_VID ) ||
+             ( product_id != 0 && product_id != device.pid ) )
+        {
+            continue;
+        }
+
+        /* See if there are any devices we should skip in enumeration */
+        if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_BLUETOOTH, VALVE_USB_VID, device.pid, 0, 0)) {
+            continue;
+        }
+
 			struct hid_device_info *device_info = create_device_info_for_hid_device(device);
 			device_info->next = root;
 			root = device_info;
 		}
-	}
 	return root;
 }}
 
@@ -975,7 +1086,7 @@ int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char
 	if ( !device_handle.connected )
 		return -1;
 
-	return [device_handle send_feature_report:(hidFeatureReport *)(void *)data];
+    return [device_handle send_feature_report:(hidFeatureReport *)(void *)data length:length];
 }
 
 int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length)
@@ -985,7 +1096,7 @@ int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data,
 	if ( !device_handle.connected )
 		return -1;
 
-	size_t written = [device_handle get_feature_report:data[0] into:data];
+	size_t written = [device_handle get_feature_report:data[0] into:data length:length];
 
 	return written == length-1 ? (int)length : (int)written;
 }
@@ -1018,7 +1129,7 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t
 		NSLog( @"hid_read_timeout with non-zero wait" );
 	}
 	int result = (int)[device_handle read_input_report:data];
-#if FEATURE_REPORT_LOGGING
+#if 0 //FEATURE_REPORT_LOGGING
 	NSLog( @"HIDBLE:hid_read_timeout (%d) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]", result,
 		  data[1], data[2], data[3], data[4], data[5], data[6],
 		  data[7], data[8], data[9], data[10], data[11], data[12],
diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c
index cab6221d1241e..b63a47a8791e2 100644
--- a/src/joystick/SDL_gamepad.c
+++ b/src/joystick/SDL_gamepad.c
@@ -1252,6 +1252,9 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
         if (SDL_IsJoystickSteamController(vendor, product)) {
             // Steam controllers have 2 back paddle buttons
             SDL_strlcat(mapping_string, "paddle1:b11,paddle2:b12,", sizeof(mapping_string));
+        } else if (SDL_IsJoystickSteamTriton(vendor, product)) {
+            // Steam controllers have 2 back paddle buttons
+            SDL_strlcat(mapping_string, "misc1:b11,paddle1:b12,paddle2:b13,paddle3:b14,paddle4:b15", sizeof(mapping_string));
         } else if (SDL_IsJoystickNintendoSwitchPro(vendor, product) ||
                    SDL_IsJoystickNintendoSwitchProInputOnly(vendor, product)) {
             // Nintendo Switch Pro controllers have a screenshot button
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index d09992828205a..831cc541caf93 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -3295,6 +3295,12 @@ bool SDL_IsJoystickSteamDeck(Uint16 vendor_id, Uint16 product_id)
     return eType == k_eControllerType_SteamControllerNeptune;
 }
 
+bool SDL_IsJoystickSteamTriton(Uint16 vendor_id, Uint16 product_id)
+{
+    EControllerType eType = GuessControllerType(vendor_id, product_id);
+    return eType == k_eControllerType_SteamControllerTriton;
+}
+
 bool SDL_IsJoystickXInput(SDL_GUID guid)
 {
     return (guid.data[14] == 'x') ? true : false;
diff --git a/src/joystick/SDL_joystick_c.h b/src/joystick/SDL_joystick_c.h
index c6e1a7b792746..f1866164548bc 100644
--- a/src/joystick/SDL_joystick_c.h
+++ b/src/joystick/SDL_joystick_c.h
@@ -144,6 +144,9 @@ extern bool SDL_IsJoystickFlydigiController(Uint16 vendor_id, Uint16 product_id)
 // Function to return whether a joystick is a Steam Deck
 extern bool SDL_IsJoystickSteamDeck(Uint16 vendor_id, Uint16 product_id);
 
+// Function to return whether a joystick is a Steam Triton
+extern bool SDL_IsJoystickSteamTriton(Uint16 vendor_id, Uint16 product_id);
+
 // Function to return whether a joystick guid comes from the XInput driver
 extern bool SDL_IsJoystickXInput(SDL_GUID guid);
 
diff --git a/src/joystick/controller_list.h b/src/joystick/controller_list.h
index 4abd8135519da..080983fa471ad 100644
--- a/src/joystick/controller_list.h
+++ b/src/joystick/controller_list.h
@@ -588,9 +588,9 @@ static const ControllerDescription_t arrControllers[] = {
 	{ MAKE_CONTROLLER_ID( 0x20d6, 0xa715 ), k_eControllerType_SwitchInputOnlyController, NULL },  // Power A Fusion Wireless Arcade Stick (USB Mode) Over BT is shows up as 057e 2009
 	{ MAKE_CONTROLLER_ID( 0x20d6, 0xa716 ), k_eControllerType_SwitchInputOnlyController, NULL },  // PowerA Nintendo Switch Fusion Pro Controller - USB requires toggling switch on back of device
 	{ MAKE_CONTROLLER_ID( 0x20d6, 0xa718 ), k_eControllerType_SwitchInputOnlyController, NULL },  // PowerA Nintendo Switch Nano Wired Controller
-    { MAKE_CONTROLLER_ID( 0x33dd, 0x0001 ), k_eControllerType_SwitchInputOnlyController, NULL },  // ZUIKI MasCon for Nintendo Switch Black
-    { MAKE_CONTROLLER_ID( 0x33dd, 0x0002 ), k_eControllerType_SwitchInputOnlyController, NULL },  // ZUIKI MasCon for Nintendo Switch ??
-    { MAKE_CONTROLLER_ID( 0x33dd, 0x0003 ), k_eControllerType_SwitchInputOnlyController, NULL },  // ZUIKI MasCon for Nintendo Switch Red
+	{ MAKE_CONTROLLER_ID( 0x33dd, 0x0001 ), k_eControllerType_SwitchInputOnlyController, NULL },  // ZUIKI MasCon for Nintendo Switch Black
+	{ MAKE_CONTROLLER_ID( 0x33dd, 0x0002 ), k_eControllerType_SwitchInputOnlyController, NULL },  // ZUIKI MasCon for Nintendo Switch ??
+	{ MAKE_CONTROLLER_ID( 0x33dd, 0x0003 ), k_eControllerType_SwitchInputOnlyController, NULL },  // ZUIKI MasCon for Nintendo Switch Red
 
 	// Valve products
 	{ MAKE_CONTROLLER_ID( 0x0000, 0x11fb ), k_eControllerType_MobileTouch, NULL },	// Streaming mobile touch virtual controls
@@ -603,4 +603,8 @@ static const ControllerDescription_t arrControllers[] = {
 	{ MAKE_CONTROLLER_ID( 0x28de, 0x1201 ), k_eControllerType_SteamControllerV2, NULL },	// Valve wired Steam Controller (HEADCRAB)
 	{ MAKE_CONTROLLER_ID( 0x28de, 0x1

(Patch may be truncated, please check the link at the top of this post.)