-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Gamepad Support #18445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Gamepad Support #18445
Conversation
You can test this PR using the following package version. |
Please read the following Contributor License Agreement (CLA). If you agree with the CLA, please reply with the following:
Contributor License AgreementContribution License AgreementThis Contribution License Agreement ( “Agreement” ) is agreed to by the party signing below ( “You” ), 1. Definitions. “Code” means the computer software code, whether in human-readable or machine-executable form, “Project” means any of the projects owned or managed by AvaloniaUI OÜ and offered under a license “Submit” is the act of uploading, submitting, transmitting, or distributing code or other content to any “Submission” means the Code and any other copyrightable material Submitted by You, including any 2. Your Submission. You must agree to the terms of this Agreement before making a Submission to any 3. Originality of Work. You represent that each of Your Submissions is entirely Your 4. Your Employer. References to “employer” in this Agreement include Your employer or anyone else 5. Licenses. a. Copyright License. You grant AvaloniaUI OÜ, and those who receive the Submission directly b. Patent License. You grant AvaloniaUI OÜ, and those who receive the Submission directly or c. Other Rights Reserved. Each party reserves all rights not expressly granted in this Agreement. 6. Representations and Warranties. You represent that You are legally entitled to grant the above 7. Notice to AvaloniaUI OÜ. You agree to notify AvaloniaUI OÜ in writing of any facts or 8. Information about Submissions. You agree that contributions to Projects and information about 9. Governing Law/Jurisdiction. This Agreement is governed by the laws of the Republic of Estonia, and 10. Entire Agreement/Assignment. This Agreement is the entire agreement between the parties, and AvaloniaUI OÜ dedicates this Contribution License Agreement to the public domain according to the Creative Commons CC0 1. |
Depending on how much you feel like over engineering/abstracting things, you could possibly not name them at all. Steam Input for example gets games to register actions rather than specific buttons. Then it works out how to trigger those actions based on specific controllers/buttons. That way no game has to worry about specific buttons, and Valve can also deal with each controller type individually if they need to (even if it's some very unique controller with non traditional buttons/layouts). https://partner.steamgames.com/doc/features/steam_controller/concepts Obviously you would still need a way to define the mapping somewhere. |
I'm not convinced it would make sense for Avalonia itself as a library to provide a set of actions as actions are in the domain of the application/game itself. It wouldn't be up for us to decide the set of actions, that would be something the application developer would or could build atop of what we provide. As fun as it is to overengineer, I think if we want to deliver this, perhaps a dash of simplicity to taste would be what's called for instead. |
Valve does this. The game defines the action name (and therefore what it does) via a config file (which also specifies the expected type of input, such as digital or analogue). Games can also provide a default binding for common controllers. Users can also override the bindings transparently to the game (since the game only cares about the actions). I will admit though, Valve's approach is also fairly complex since it also allows for context aware bindings (EG different controls for when in a game menu compared to the actual game). But that's probably not needed here. I personally would want to move away from specific key events anyway in most cases. I know it's a legacy approach from WPF, but it does not change the fact that it makes developers make (often bad) assumptions about input devices. EG not everybody uses QWERTY keyboards, nor do they use controllers that have analogue triggers. What might be a sensible binding for one person will be awful for someone else. I have seen some game engines in recent years actually refuse to directly expose raw input events for that exact reason. |
Nice work so far. Some thought about the intended uses of gamepad input is required before further steps are taken. Discussions in the feature request suggest that this feature is constrained to providing input for an event-driven GUI, not a real-time simulation/game. But in this PR you have a goal of creating "a small game". I'd advise against this, as piping real-time input through a windowing event loop will never work properly. Avalonia's rendering and input processing models will fight against you. If you want real-time, run a frame loop, poll for input within it, and draw directly on the GPU. There are more substantial issues beyond this. A huge challenge is that with a mouse we know what the user wants to interact with because they are pointing at it. But a gamepad steers instead of points, and there are many different ways to interpret steering.
Add to this the existing discussions about how gamepad layouts are different, and about how different cultures interpret input of the exact same gamepad differently (Japan's A/B & O/X switch). The conclusion I draw is that it's reasonable to expose a stream of button presses on the gamepad, but nowhere near enough. There is still a tonne of design and implementation work before those events can be turned into UI interactions. Do we want to put that on the plate of the Avalonia consumer? I don't think so. I would like to see gamepad input support in Avalonia extend all the way to actually navigating through a live UI. My recommendation is designing public APIs which expose all the necessary information to implement at least the three paradigms above, and then implementing them as a drop-in component for a TopLevel. The job of each implementation is to entirely encapsulate gamepad input and turn it into semantic Avalonia events raised on specific objects in the visual/logical tree. |
I will also add to this: There are several ways to emulate a mouse pointer with different input sources and you can't really assume a single way is the "best way". EG Steam Controllers have touch pads on them which can be mapped directly to mouse movement. Xbox controllers however have joysticks, and NES controllers have neither. Each of those would need completely differently logic. Although I am not sure if direct pointer emulation should be the goal here (at that point you may as well just write a platform specific userspace mouse driver and not touch Avalonia's code at all). |
Another thing: SDL 3 uses these names for buttons: https://wiki.libsdl.org/SDL3/SDL_GamepadButton All buttons can also be rebound by the user via a config file (specified via an environment variable), or a developer by code. https://wiki.libsdl.org/SDL3/SDL_AddGamepadMapping Which is also probably a somewhat reasonable compromise if the "action" approach is too complicated. This also has the advantage of allowing new controllers to be supported without having to update the app. But I am still not a fan of it assuming a finite set of buttons exist. |
I did mean a sample in the control catalogue to "show off" the feature. I'd have to actually create a GameObject and "play around with it" to determine if the rendering and input processing you mention would fight against me so. |
@@ -1782,6 +1783,80 @@ public enum LayeredWindowFlags | |||
|
|||
[DllImport("user32.dll")] | |||
public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, LayeredWindowFlags dwFlags); | |||
[DllImport("user32", ExactSpelling = true)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use CsWin32 for new interop methods and NativeMethods.txt?
We slowly migrate out from hand-written pinvokes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use CsWin32 for new interop methods and NativeMethods.txt? We slowly migrate out from hand-written pinvokes.
I should be able to, but that won't handle when XInput v 1.4 isn't available for a fallback to XInput v 9.1.0
That's still the recommended way to handle XInput if you're not shipping your own native binaries or statically linking your specific version of XInput.
For XInput specifically, these hand-written invocations might need to remain, but the actual structs themselves were copied from source-generated output of another open-source project (TerraFX.Interop.Windows)
I ran my approach for LoadLibrary xinput1_4.dll, if that fails fallback to xinput9_1_0.dll - the layout and size of the structs are the same across all versions and this approach will succeed in all versions of windows dating back to Windows XP without code modifications.
/// <summary> | ||
/// The static entry-point to interacting with Gamepads. This is null if the current platform does not have a Gamepad implementation. | ||
/// </summary> | ||
public static GamepadManager? Instance { get; protected set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally should be a TopLevel bound API instead of global statics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which is actually similar to window.Gamepad
essentially on browser
}); | ||
} | ||
|
||
public IObservable<GamepadEventArgs> GamepadStream => _gamepadStream; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, is it supposed to be routed event API or observable API?
We will need to discuss this API on the API review later. But I am just asking early questions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, is it supposed to be routed event API or observable API? We will need to discuss this API on the API review later. But I am just asking early questions.
Yeah, I am being pulled in both directions - the Observable API is great for streaming these events, but if we want them to route on the UI layer, they would need to be routable events.
I am considering splitting these two up, with an IObservable that doesn't get routed, and if not "handled" at this level, producing routed-events from these - in line with what TomEdwards was saying above - turning these "raw" gamepad events into Semantic Avalonia events that can work with a UI.
} | ||
|
||
public IObservable<GamepadEventArgs> GamepadStream => _gamepadStream; | ||
public IReadOnlyList<GamepadState> GetSnapshot() => [.. _currentState]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetSnapshot doesn't seem to add any value here, as it only reads last already recorded state. Without requesting current value from the system directly (so, similarly to how we don't have unreliable global mouse state in Avalonia).
Value == state.Value; | ||
} | ||
|
||
public readonly override int GetHashCode() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make it a record struct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great suggestion! Taken and implemented.
// this struct is a glorified fixed-buffer and these "unused fields" will be used. | ||
public struct GamepadButtons | ||
{ | ||
private ButtonState _0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, we can use actual fixed-buffer structs on netstandard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, but we don't have MemoryMarshal.CreateSpan(T, Int32) in .Net Standard 2.0
So I would still need to venture off the happy-path for spans to keep NS2.0 compatibility. :(
You can test this PR using the following package version. |
In an initial quick test for input latency, I used the following setup OBS Recording my microphone near the controller, and Window Capture (via Windows 10 Composition Capture). Splitting the recorded file into audio and video frames, doing some analysis. I can look at the audio stream for when my controller clicked, and I can look at the video frames to see when the entity actually moved and was rendered. The click was observed at 0.465 seconds into the footage, but the movement was seen on a frame at 0.467 seconds... there's probably a delay from the USB microphone, so it's coming in at maybe a frame or so of latency. It reacts really quickly, and I don't have any good ways of measuring input latency when you're using a separate render-thread. Forcing it into a single thread and using DXGI, it feels really responsive, more responsive than some of the games I've played. |
On Android, gamepad input is treated the same way as key events from keyboard. They are handled on DispatchKeyEvents for buttons and DispatchGenericMotionEvent for axes. It would be easier to extend our existing key events to cover gamepad input. |
Also, please fixup and author your commits using your github user. The CLA bot doesn't work well with email users. |
Your git modules are broken. |
The amount of latency between the control and windows is irrelevant. At least, it's out of control of Avalonia. Could you use a script to press the button and use that to time how long it takes for it to register in your app? |
Squashing many things
63713b2
to
ac346b9
Compare
I've attempted to squash the commits under my github profile. Does it look better now? |
Yes. It good now. You only need to accept the CLA. |
Pull Request is still in progress and this should be considered a draft.
Things I want clarification on
Button Names
What should the Capture API look like? We want to provide a robust way to enable background processing of Controller updates and events even when the UIThread dispatcher is being held up.
Gamepads
I am restricting this to specifically Controllers and Gamepads. They don't have to conform exactly to the Standard Gamepad
but I am narrowing the scope of this to make it saner to implement - supporting literally every device under the sun on all of the platforms would be too grand of an undertaking for me.
To-do list
Tentative Requirements
(Partially elicited through conversations with various Avalonia team members)
What does the pull request do?
This pull request adds Gamepad support for AvaloniaUI.
What is the current behavior?
There is no gamepad support.
What is the updated/expected behavior with this PR?
How was the solution implemented (if it's not obvious)?
Creating an Interface IGamepadManager and abstract class GamepadManager to manage gamepads.
The entry-point to getting Gamepad events is GamepadManager.Instance which is nullable and returns null if the current platform does not support gamepads (e.g.: Headless)
An implementation of this per-platform, since each major platform has its own quirks and particularities for Gamepads.
Using only managed C# code interface with the system level APIs at a low-level.
For Windows, one must dual-wield RawInput HID and XInput for full support. This appears to be the best way to have broad-scale support ranging from Windows 7 to Windows 11 without losing support for any particular kind of device. WGI (Windows.Gaming.Input) doesn't function when the window is out-of-focus, and would put requirements on the version of windows if we wanted to integrate with it. GameInput is not yet production ready in my opinion, it's still part of the GDK and not subsumed into the Windows SDK and it looks like the license for GameInput.h isn't friendly to OSS which is problematic, and GameInput has bugs in it and doesn't work with bluetooth controllers. DirectInput is feasible but more complex to use for C# compared with RawInput, so RawInput it is.
For Linux libudev will be used
For Android, there's probably a similar low-level API, needs research and impl.
For Apple MacOS and iOS [iOS 7.0+ iPadOS 7.0+ Mac Catalyst 13.1+ macOS 10.9+ tvOS 9.0+ visionOS 1.0+], https://developer.apple.com/documentation/gamecontroller?language=objc
For Web, there's the w3c gamepad API https://w3c.github.io/gamepad
Checklist
Breaking changes
None
Obsoletions / Deprecations
None
Fixed issues
Satisfies #6945