Skip to content

Commit c199d9d

Browse files
committed
complete xbone gamepad implementation
remove InputMarshal (as it is unnecessary and makes it more difficult to trace allocations) several fixes
1 parent b270577 commit c199d9d

11 files changed

Lines changed: 194 additions & 759 deletions

File tree

sources/Input/Input/ButtonReadOnlyList.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections;
2+
using System.Runtime.CompilerServices;
23

34
namespace Silk.NET.Input;
45

@@ -44,4 +45,7 @@ public ButtonReadOnlyList(IReadOnlyList<Button<T>> buttonList, Func<int, int>? i
4445

4546
/// <inheritdoc />
4647
public Button<T> this[int index] => List[_indexMap(index)];
48+
49+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
50+
internal IReadOnlyList<Button<T>> CreateListCopy() => List.ToArray();
4751
}

sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlGamepad.cs

Lines changed: 152 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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;
45
using System.ComponentModel;
56
using System.Diagnostics;
67
using 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
}

sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.Extended.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ internal void UpdateRawButtonState(JoystickButton button, bool isDown, float pre
6262
[MethodImpl(MethodImplOptions.AggressiveInlining)]
6363
internal Vector2 GetAxisStateByIndex2D(int xIndex, int yIndex) => new(GetAxisStateByIndex(xIndex), GetAxisStateByIndex(yIndex));
6464

65-
internal bool UpdateRawAxisState(JoystickAxis axis, float value, out JoystickAxisMoveEvent evt, ulong sdlTimestamp, long timestamp)
65+
internal bool UpdateRawAxisState(JoystickAxis axis, float value, ulong sdlTimestamp, long timestamp,
66+
out JoystickAxisMoveEvent evt)
6667
{
6768
var index = axis.Index();
6869
if (index < 0)

sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPointerDevice.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ protected void AddButtonEvent(PointerButton button, long timestamp, ulong sdlTim
3636

3737
while (idx >= _buttons.Count)
3838
{
39-
_buttons.Add(_unknownButton);
39+
_buttons.Add(new Button<PointerButton>(PointerButton.Unknown, false, 0f));
4040
}
4141

4242
ref var myButton = ref CollectionsMarshal.AsSpan(_buttons)[idx];
@@ -50,8 +50,6 @@ protected void AddButtonEvent(PointerButton button, long timestamp, ulong sdlTim
5050
}
5151
}
5252

53-
private static readonly Button<PointerButton> _unknownButton = new(PointerButton.Unknown, false, 0f);
54-
5553

5654
private readonly List<Button<PointerButton>> _buttons = new(EnumInfo<PointerButton>.UniqueValues.Count);
5755
protected ButtonReadOnlyList<PointerButton> Buttons => new(_buttons);

0 commit comments

Comments
 (0)