11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4+ using System . Collections . Frozen ;
45using System . ComponentModel ;
56using System . Diagnostics ;
67using System . Runtime . CompilerServices ;
@@ -23,16 +24,17 @@ private SdlGamepad(SdlJoystick joystick, nint uniqueId) : base(joystick.Backend,
2324 {
2425 Joystick = joystick ;
2526 _sdlDeviceId = ( uint ) base . SdlDeviceId ;
26-
27+ _state = null ! ; // created in Initialize method
2728 }
2829
2930 // we handle the gamepad mapping ourselves rather than relying on gamepad events from SDL
3031 // the hope is that doing it this way makes it more straightforward to manage input remapping with other backends or
3132 // adapt to unknown controllers in the future, despite being an unnecessary translation step at the moment.
3233 // hopefully this allows us to mimic SDL's mapping system for non-sdl input backends.
34+ // todo - abstract the remapping logic into a separate class that can be injected into non-sdl backends
3335 private void Remap ( GamepadHandle gamepadHandle , long timestamp , ulong sdlTimestamp )
3436 {
35- _bindings . Clear ( ) ;
37+ var bindings = new Dictionary < int , GamepadBinding > ( ) ;
3638 // _outputBindings.Clear();
3739 _hatBindings . Clear ( ) ;
3840 var bindingsCount = 0 ;
@@ -49,71 +51,114 @@ private void Remap(GamepadHandle gamepadHandle, long timestamp, ulong sdlTimesta
4951 return ;
5052 }
5153
54+ Span < int > buttonBindings = stackalloc int [ bindingsCount ] ;
55+ Span < int > axisBindings = stackalloc int [ bindingsCount ] ;
56+ Span < int > hatBindings = stackalloc int [ bindingsCount ] ;
57+ var buttonBindingsCount = 0 ;
58+ var axisBindingsCount = 0 ;
59+ var hatBindingListCount = 0 ;
5260 for ( var i = 0 ; i < bindingsCount ; i ++ )
5361 {
54- var binding = mappings [ i ] ;
62+ ref var binding = ref Unsafe . AsRef < GamepadBinding > ( mappings [ i ] ) ;
5563
56- if ( binding -> OutputType == GamepadBindingType . None )
64+ if ( binding . OutputType == GamepadBindingType . None )
5765 {
5866 continue ;
5967 }
6068
61-
62- switch ( binding ->InputType )
69+ switch ( binding . InputType )
6370 {
6471 case GamepadBindingType . Button :
6572 {
66- var id = InputIndexToMappingIndex ( binding -> Input . Button , InputType . Button ) ;
67- if ( AddBinding ( id , binding ) )
73+ var id = InputIndexToMappingIndex ( binding . Input . Button , InputType . Button ) ;
74+ if ( AddBinding ( id , binding , bindings ) )
6875 {
69- // doing this here lets us pre-populate our axes for consumers to know what buttons are present
70- UpdateFromJoyButton ( binding ->Input . Button , false , sdlTimestamp , timestamp ) ;
76+ buttonBindings [ buttonBindingsCount ++ ] = binding . Input . Button ;
7177 }
7278 break ;
7379 }
7480 case GamepadBindingType . Axis :
7581 {
76- var id = InputIndexToMappingIndex ( binding -> Input . Axis . Axis , InputType . Axis ) ;
77- if ( AddBinding ( id , binding ) )
82+ var id = InputIndexToMappingIndex ( binding . Input . Axis . Axis , InputType . Axis ) ;
83+ if ( AddBinding ( id , binding , bindings ) )
7884 {
79- // doing this here lets us pre-populate our axes for consumers to know what buttons are present
80- UpdateFromJoyAxis ( binding ->Input . Axis . Axis , 0 , sdlTimestamp , timestamp ) ;
85+ axisBindings [ axisBindingsCount ++ ] = binding . Input . Axis . Axis ;
8186 }
87+
8288 break ;
8389 }
8490 case GamepadBindingType . Hat :
8591 {
86- var id = binding -> Input . Hat . Hat ;
92+ var id = binding . Input . Hat . Hat ;
8793
8894 while ( _hatBindings . Count <= id )
8995 {
9096 _hatBindings . Add ( null ) ;
9197 }
9298
93- _hatBindings [ id ] ??= [ ] ;
94- _hatBindings [ id ] ! . Add ( * binding ) ;
99+ var list = _hatBindings [ id ] ;
100+ if ( list is null )
101+ {
102+ hatBindings [ hatBindingListCount ++ ] = id ;
103+ _hatBindings [ id ] = list = [ ] ;
104+ }
105+
106+ list . Add ( binding ) ;
107+ continue ;
108+ }
109+ default :
110+ {
95111 continue ;
96112 }
97113 }
98114 }
99115
100116 NativeBackend . Free ( mappings ) ;
117+ _bindings = bindings . ToFrozenDictionary ( ) ;
118+
119+ // update with default states
120+ // doing this here lets us pre-populate our axes for consumers to know what buttons are present
121+ for ( var i = 0 ; i < buttonBindingsCount ; ++ i )
122+ {
123+ var which = buttonBindings [ i ] ;
124+ var on = NativeBackend . GetJoystickButton ( Joystick . JoystickHandle , which ) ;
125+ UpdateFromJoyButton ( which , on , sdlTimestamp , timestamp ) ;
126+ }
127+
128+ for ( var i = 0 ; i < axisBindingsCount ; ++ i )
129+ {
130+ var which = axisBindings [ i ] ;
131+
132+ short state = 0 ;
133+ if ( NativeBackend . GetJoystickAxisInitialState ( Joystick . JoystickHandle , which , & state ) != 0 )
134+ {
135+ InputLog . Error ( "Failed to get initial axis state" ) ;
136+ }
137+
138+ UpdateFromJoyAxis ( which , state , sdlTimestamp , timestamp ) ;
139+ }
140+
141+ for ( var i = 0 ; i < hatBindingListCount ; ++ i )
142+ {
143+ // todo: grab actual state from sdl if it's available?)
144+ var which = hatBindings [ i ] ;
145+ Debug . Assert ( _hatBindings [ which ] != null ) ;
146+ UpdateFromJoyHat ( which , SdlJoystick . HatState . Centered , sdlTimestamp , timestamp ) ;
147+ }
148+
101149 return ;
102150
103- bool AddBinding ( int id , GamepadBinding * binding )
151+ static bool AddBinding ( int id , in GamepadBinding binding , Dictionary < int , GamepadBinding > bindings )
104152 {
105- switch ( binding -> OutputType )
153+ switch ( binding . OutputType )
106154 {
107155 case GamepadBindingType . Axis :
108- _bindings . Add ( id , * binding ) ;
109- return true ;
110156 case GamepadBindingType . Button :
111- _bindings . Add ( id , * binding ) ;
157+ bindings . Add ( id , binding ) ;
112158 return true ;
113159 default :
114160 return false ;
115161 }
116-
117162 }
118163 }
119164
@@ -211,7 +256,8 @@ protected override void Release()
211256 if ( backend . AttemptUniqueId ( gpn , ref joystickUniqueId ) )
212257 {
213258 return new SdlGamepad ( joystick , uniqueId : joystickUniqueId ) {
214- _thumbstickEvents = context . GamepadThumbstickMoveSdlEvents
259+ ThumbstickEvents = context . GamepadThumbstickMoveSdlEvents ,
260+ TriggerEvents = context . GamepadTriggerMoveSdlEvents ,
215261 } ;
216262 }
217263
@@ -220,20 +266,43 @@ protected override void Release()
220266 if ( backend . AttemptUniqueId ( guid , ref joystickUniqueId ) )
221267 {
222268 return new SdlGamepad ( joystick , uniqueId : joystickUniqueId ) {
223- _thumbstickEvents = context . GamepadThumbstickMoveSdlEvents
269+ ThumbstickEvents = context . GamepadThumbstickMoveSdlEvents ,
270+ TriggerEvents = context . GamepadTriggerMoveSdlEvents ,
224271 } ;
225272 }
226273
227274 joystickUniqueId = SdlInputBackend . FallbackUniqueId < SdlGamepad > ( sdlDeviceId , joystickUniqueId ) ;
228275 var sdlGamepad = new SdlGamepad ( joystick , uniqueId : joystickUniqueId ) {
229- _thumbstickEvents = context . GamepadThumbstickMoveSdlEvents
276+ ThumbstickEvents = context . GamepadThumbstickMoveSdlEvents ,
277+ TriggerEvents = context . GamepadTriggerMoveSdlEvents ,
230278 } ;
231279 return sdlGamepad ;
232280 }
233281
234282 private void UpdateGamepadAxis ( GamepadAxis gAxis , int value , int min , int max , ulong sdlTimestamp , long timestamp )
235283 {
236- var mappedValue = ( float ) ( value + min ) / ( max - min ) ;
284+ var lower = Math . Min ( min , max ) ;
285+ var upper = Math . Max ( min , max ) ;
286+ value = Math . Clamp ( value , lower , upper ) ;
287+
288+ var mappedValue = NormalizeInRange ( value , min , max ) ;
289+ UpdateGamepadAxis ( gAxis , mappedValue , sdlTimestamp , timestamp ) ;
290+
291+ return ;
292+ static float NormalizeInRange ( int current , int rangeMin , int rangeMax )
293+ {
294+ if ( rangeMin == rangeMax )
295+ {
296+ return 0f ;
297+ }
298+
299+ var normalized = ( current - rangeMin ) / ( float ) ( rangeMax - rangeMin ) ;
300+ return Math . Clamp ( normalized , 0f , 1f ) ;
301+ }
302+ }
303+
304+ private void UpdateGamepadAxis ( GamepadAxis gAxis , float mappedValue , ulong sdlTimestamp , long timestamp )
305+ {
237306 switch ( gAxis )
238307 {
239308 case GamepadAxis . Invalid :
@@ -246,29 +315,29 @@ private void UpdateGamepadAxis(GamepadAxis gAxis, int value, int min, int max, u
246315 var yIndex = axes . Y . Index ( ) ;
247316 var previous = Joystick . GetAxisStateByIndex2D ( xIndex , yIndex ) ;
248317
249- if ( Joystick . UpdateRawAxisState ( axis , mappedValue , out _ , sdlTimestamp , timestamp ) )
318+ if ( Joystick . UpdateRawAxisState ( axis , mappedValue , sdlTimestamp , timestamp , out _ ) )
250319 {
251320 var latest = Joystick . GetAxisStateByIndex2D ( xIndex , yIndex ) ;
252- _thumbstickEvents . Enqueue ( new GamepadThumbstickMoveEvent ( Gamepad : this ,
321+ ThumbstickEvents . Enqueue ( new GamepadThumbstickMoveEvent ( Gamepad : this ,
253322 Timestamp : timestamp ,
254323 Value : latest ,
255324 Delta : latest - previous ) , sdlTimestamp ) ;
256325
257326 ToSplitPair ( axis , out var minusAxis , out var plusAxis ) ;
258327 var split = SdlJoystick . SplitValue ( mappedValue ) ;
259- Joystick . UpdateRawAxisState ( minusAxis , split . X , out _ , sdlTimestamp , timestamp ) ;
260- Joystick . UpdateRawAxisState ( plusAxis , split . Y , out _ , sdlTimestamp , timestamp ) ;
328+ Joystick . UpdateRawAxisState ( minusAxis , split . X , sdlTimestamp , timestamp , out _ ) ;
329+ Joystick . UpdateRawAxisState ( plusAxis , split . Y , sdlTimestamp , timestamp , out _ ) ;
261330 }
262331
263332 break ;
264333
265334 }
266335 case GamepadAxis . LeftTrigger or GamepadAxis . RightTrigger :
267336 {
268- if ( Joystick . UpdateRawAxisState ( ToJoystickAxis ( gAxis ) , mappedValue , out var moveEvt , sdlTimestamp , timestamp ) )
337+ if ( Joystick . UpdateRawAxisState ( ToJoystickAxis ( gAxis ) , mappedValue , sdlTimestamp , timestamp , out var moveEvt ) )
269338 {
270- _triggerEvents . Enqueue ( new GamepadTriggerMoveEvent ( this , moveEvt . Timestamp , moveEvt . Axis ,
271- moveEvt . Value , moveEvt . Delta ) ) ;
339+ TriggerEvents . Enqueue ( new GamepadTriggerMoveEvent ( this , moveEvt . Timestamp , moveEvt . Axis ,
340+ moveEvt . Value , moveEvt . Delta ) , sdlTimestamp ) ;
272341 }
273342
274343 break ;
@@ -354,12 +423,6 @@ public void UpdateFromJoyButton(int buttonIdx, bool down, ulong sdlTimestamp, lo
354423 }
355424 }
356425
357- public void AddButtonEvent ( byte sdlButtonId , byte sdlButtonDown , ulong sdlTimestamp , long timestamp ) =>
358- UpdateButton ( ( GamepadButton ) sdlButtonId , sdlButtonDown > 0 , sdlTimestamp , timestamp ) ;
359-
360- public void AddAxisEvent ( byte evtAxis , short evtValue , ulong sdlTimestamp , long timestamp ) =>
361- UpdateGamepadAxis ( ( GamepadAxis ) evtAxis , evtValue , Sdl . JoystickAxisMin , Sdl . JoystickAxisMax , sdlTimestamp , timestamp ) ;
362-
363426 public void UpdateFromJoyAxis ( int axis , short joystickInput , ulong sdlTimestamp , long timestamp )
364427 {
365428 if ( ! _bindings . TryGetValue ( InputIndexToMappingIndex ( axis , InputType . Axis ) , out var binding ) )
@@ -375,11 +438,50 @@ public void UpdateFromJoyAxis(int axis, short joystickInput, ulong sdlTimestamp,
375438 switch ( binding . OutputType )
376439 {
377440 case GamepadBindingType . Axis :
378- UpdateGamepadAxis ( output ->Axis . Axis , joystickInput , input ->AxisMin , input ->AxisMax , sdlTimestamp , timestamp ) ;
441+ {
442+ var inputPercent = NormalizeInRange ( joystickInput , input ->AxisMin , input ->AxisMax ) ;
443+
444+ ref readonly var outputAxis = ref output ->Axis ;
445+ var outputRaw = MapNormalizedToRange ( inputPercent , outputAxis . AxisMin , outputAxis . AxisMax ) ;
446+ UpdateGamepadAxis ( output ->Axis . Axis , outputRaw , outputAxis . AxisMin , outputAxis . AxisMax , sdlTimestamp , timestamp ) ;
379447 break ;
448+ }
380449 case GamepadBindingType . Button :
381- UpdateButton ( output ->Button , joystickInput >= input ->AxisMin && joystickInput <= input ->AxisMax , sdlTimestamp , timestamp ) ;
450+ {
451+ UpdateButton ( output ->Button , IsInPressedRange ( joystickInput , input ->AxisMin , input ->AxisMax ) , sdlTimestamp , timestamp ) ;
382452 break ;
453+ }
454+ }
455+
456+ return ;
457+
458+ static int MapNormalizedToRange ( float normalized , int min , int max )
459+ {
460+ var mapped = min + ( ( max - min ) * normalized ) ;
461+ var lower = Math . Min ( min , max ) ;
462+ var upper = Math . Max ( min , max ) ;
463+ return Math . Clamp ( ( int ) MathF . Round ( mapped ) , lower , upper ) ;
464+ }
465+
466+ static float NormalizeInRange ( int value , int min , int max )
467+ {
468+ if ( min == max )
469+ {
470+ if ( value < min )
471+ {
472+ return 0 ;
473+ }
474+
475+ if ( value > max )
476+ {
477+ return 1 ;
478+ }
479+
480+ return 0.5f ;
481+ }
482+
483+ var normalized = ( value - min ) / ( float ) ( max - min ) ;
484+ return Math . Clamp ( normalized , 0f , 1f ) ;
383485 }
384486 }
385487
@@ -416,7 +518,7 @@ public void UpdateFromJoyHat(int hatIdx, SdlJoystick.HatState hatState, ulong sd
416518 break ;
417519 case GamepadBindingType . Button :
418520 var button = binding . Output . Button ;
419- UpdateButton ( button , bindingState != SdlJoystick . HatState . Centered , sdlTimestamp , timestamp ) ; ;
521+ UpdateButton ( button , bindingState != SdlJoystick . HatState . Centered , sdlTimestamp , timestamp ) ;
420522 break ;
421523 }
422524 }
@@ -452,14 +554,16 @@ static JoystickButton AsJoystickButton(GamepadButton buttonIndex) =>
452554 } ;
453555 }
454556
455- // todo - frozen dictionary? this dictionary won't frequently be updated, and when it is it can afford to be rebuilt
456- private readonly Dictionary < int , GamepadBinding > _bindings = new ( ) ;
557+ private FrozenDictionary < int , GamepadBinding > _bindings ;
457558 private readonly List < List < GamepadBinding > ? > _hatBindings = [ ] ;
458- // private readonly List<GamepadBinding> _outputBindings = [];
459- internal required ISdlEventQueue < GamepadThumbstickMoveEvent > _thumbstickEvents { get ; init ; }
460- private readonly Queue < GamepadTriggerMoveEvent > _triggerEvents = new ( ) ;
559+ internal required ISdlEventQueue < GamepadThumbstickMoveEvent > ThumbstickEvents { get ; init ; }
560+ internal required ISdlEventQueue < GamepadTriggerMoveEvent > TriggerEvents { get ; init ; }
461561
462562
463563 JoystickState IJoystick . State => Joystick . State ;
464564 ButtonReadOnlyList < JoystickButton > IButtonDevice < JoystickButton > . State => GamepadState . Buttons ;
565+
566+ // Keep the lower endpoint exclusive to preserve the existing overlap behavior for button-like axis mappings.
567+ private static bool IsInPressedRange ( int value , int min , int max ) =>
568+ min <= max ? value > min && value <= max : value < min && value >= max ;
465569}
0 commit comments