Compare commits

..
Author SHA1 Message Date
Cameron Gutman 821d4ac9dd Version 0.5.1 (hotfix for Windows and Mac only) 2018-09-16 12:18:58 -07:00
Cameron Gutman f4408473e6 Fix broken Alt+Tab behavior. Fixes #72 2018-09-16 11:21:15 -07:00
Cameron Gutman 1f759577ce Version 0.5.0 2018-09-15 16:23:10 -07:00
Cameron Gutman 04376ca047 Fix Clang format string signness mismatch warnings 2018-09-15 16:00:36 -07:00
Cameron Gutman 7ac108f495 Reduce delay for PCs appearing online 2018-09-15 13:43:02 -07:00
Cameron Gutman a2eece0f5c Log audio buffer size 2018-09-15 13:27:57 -07:00
Cameron Gutman a03279df3b Fix errant mouse button event being sent on mouse capture 2018-09-14 22:09:45 -07:00
Cameron Gutman bef6c2b665 Fix the underflow caused by the underflow fix :( 2018-09-14 21:03:06 -07:00
Cameron Gutman 8002c65693 Block Intel drivers earlier than 4836 from using HEVC due to artifacting issues. Fixes #32 2018-09-14 20:20:47 -07:00
Cameron Gutman 5c7247b8b5 Archive symbols from prebuilt libraries too 2018-09-14 19:32:23 -07:00
Cameron Gutman 1a18e68441 Make Windows shortcut description consistent with Mac and Linux 2018-09-14 19:23:02 -07:00
Cameron Gutman 72afa55897 Add tip text to the StreamSegue telling users about the quit shortcut. Fixes #77 2018-09-14 19:11:06 -07:00
Cameron Gutman eed9fca10c Mass conversion of isNull() -> isEmpty() 2018-09-14 18:36:15 -07:00
Cameron Gutman 25d97e187a Fix large mouse input delay on GFE 3.14.0 and earlier. Fixes #68 2018-09-14 18:35:28 -07:00
Cameron Gutman 7ca2ca8045 Print the GPU driver version on Windows 2018-09-13 14:54:30 -07:00
Cameron Gutman 54885c3e5f Revert "Delay Session::exec() for 250 ms to allow the StackView transition to complete" due to regressing spinner performance on Windows and Linux
This reverts commit b8ffb6e53d.
2018-09-13 14:39:33 -07:00
Cameron Gutman b8ffb6e53d Delay Session::exec() for 250 ms to allow the StackView transition to complete 2018-09-13 14:33:38 -07:00
Cameron Gutman 7bc43c226d Fix underflow in audio drop code 2018-09-13 14:09:03 -07:00
Cameron Gutman da4697794d Increase buffer size for glitch-free playback on macOS 2018-09-13 13:02:21 -07:00
Cameron Gutman 879c524658 Revert "Process Qt events in the SDL loop" to fix input issues on Windows and Mac
This reverts commit c8e00195ed.
2018-09-13 12:14:56 -07:00
Cameron Gutman e3a8f010c3 Fixes for experimental Qt audio renderer 2018-09-13 12:00:51 -07:00
Cameron Gutman f2f8f92172 Add gamepad mapping DB from https://github.com/gabomdq/SDL_GameControllerDB 2018-09-13 11:58:32 -07:00
Cameron Gutman 265ec96c6f Fix grammar mistake in shortcut description 2018-09-13 08:18:01 -07:00
Cameron Gutman 6e06174db6 Add experimental Qt audio renderer 2018-09-13 08:17:26 -07:00
Cameron Gutman 9677569293 Fix Travis CI build 2018-09-13 08:05:54 -07:00
Cameron Gutman c8e00195ed Process Qt events in the SDL loop 2018-09-13 08:03:06 -07:00
Cameron Gutman 64bc189010 Fixes to SDL audio renderer and autodetection support 2018-09-13 07:46:01 -07:00
Cameron Gutman 12496e4432 Refactor audio rendering into a separate class 2018-09-13 06:23:06 -07:00
Cameron Gutman 9e2fd67487 Add option to enable mouse acceleration for remote desktop usage 2018-09-09 13:21:11 -07:00
Cameron Gutman af17d56cbd Add option to disable mDNS PC discovery. Fixes #50 2018-09-09 12:33:19 -07:00
Cameron Gutman 6e0a657b76 Update readme and metadata for Beta 1 features 2018-09-09 11:53:58 -07:00
Cameron Gutman b0a9fc7085 Add Help option to connection failure dialog 2018-09-09 11:53:14 -07:00
Cameron Gutman 5e47b8a835 Update README.md 2018-09-09 11:40:13 -07:00
Cameron Gutman 0b94ad1fef Add help link in Add PC failure dialog 2018-09-09 11:19:33 -07:00
Cameron Gutman 9dcd856099 Blacklist Kepler GPUs for HEVC due to hybrid decode support present in some drivers 2018-09-09 10:48:01 -07:00
Cameron Gutman cfbcc4093c Reduce UI jank when transitioning to the stream segue 2018-09-09 10:17:32 -07:00
Cameron Gutman b8169ae08b Fix broken quit dialog transition to next app 2018-09-09 10:08:23 -07:00
Cameron Gutman 103625fbdd Version v0.4.0 2018-09-08 16:58:03 -07:00
Cameron Gutman 4712080a1b Fix centering of streaming window and size after toggling full-screen on Windows 2018-09-08 16:01:35 -07:00
Cameron Gutman 93c683be3a Allow renderers that force V-sync on to run without it when streaming higher than the display refresh rate 2018-09-08 15:27:21 -07:00
Cameron Gutman c9a7c15f98 Improve unsupported FPS options and performance 2018-09-08 15:09:46 -07:00
Cameron Gutman 64a08f0533 Allow higher than display refresh rate streaming with unsupported FPS option 2018-09-08 14:34:11 -07:00
Cameron Gutman 52c6455ca5 Avoid sending a click event on focus gain on Linux 2018-09-08 14:05:42 -07:00
Cameron Gutman 238681d72f Merge pull request #67 from jhakonen/capture-on-activated-window
Capture mouse cursor when clicking inactive window
2018-09-08 14:01:27 -07:00
Cameron Gutman dbcc6feb3f Merge pull request #66 from jhakonen/fix-key-not-working
Fixed < key in keyboard not working
2018-09-08 11:05:34 -07:00
Janne Hakonen 300b53f28c Capture mouse cursor when clicking inactive window 2018-09-08 16:39:20 +03:00
Janne Hakonen 3dabb1c891 Fixed < key in keyboard not working 2018-09-08 14:16:27 +03:00
Cameron Gutman 4381f7a973 Apply changes to preferences when loading the settings page 2018-09-07 23:16:13 -07:00
Cameron Gutman 77b767ae5b Hide autodetect option for audio configuration since it's broken on macOS and Linux (and to some extent, Windows) 2018-09-07 22:51:56 -07:00
Cameron Gutman 2c4f78133f Avoid inflating MessageDialogs for each app in the grid. Fixes scrolling jank on app grid. 2018-09-07 17:33:34 -07:00
Cameron Gutman ec2c181f4e Use 4 threads for fetching box art 2018-09-07 17:30:43 -07:00
Cameron Gutman fae98eb13a Limit box art fetching to a single thread to improve UI responsiveness 2018-09-07 17:02:52 -07:00
Cameron Gutman f1d117d5d7 Ensure FFmpeg log messages are written to the debug log 2018-09-07 15:53:10 -07:00
Cameron Gutman 529ac8041a Only write a dump for the first unhandled exception 2018-09-07 15:45:35 -07:00
Cameron Gutman 391e84e0f4 Create a crash dump when Moonlight crashes on Windows 2018-09-07 14:16:59 -07:00
Cameron Gutman 6a2b717ff1 Run in native resolution on Retina displays when streaming above scaled resolution 2018-09-07 01:12:18 -07:00
Cameron Gutman 475a39cb73 Recreate the decoder when moving to a new display to allow Pacer to get the new refresh rate 2018-09-06 19:42:53 -07:00
Cameron Gutman 4dba74e2f9 Fix handling of undefined and non-matching refresh rate 2018-09-06 19:16:59 -07:00
Cameron Gutman 587d783955 Use SDL_WINDOWPOS_CENTERED_DISPLAY() to fix incorrect positioning on multi-display system on macOS 2018-09-06 18:53:00 -07:00
Cameron Gutman 3bfe87d516 Merge pull request #63 from jhakonen/optional-desktop-shortcut
Changed desktop shortcut as optional in Windows installer. Fixes #17
2018-09-06 16:20:50 -07:00
Janne Hakonen d3e43012fb Changed desktop shortcut as optional in installer. Fixes #17 2018-09-06 20:09:00 +03:00
Cameron Gutman 5da156b433 Prefer H.264 for higher decoding performance in very high load situations 2018-09-05 18:57:40 -07:00
Cameron Gutman b2eeb63b6f Mark the full-screen display mode as recommended due to lower latency and better frame pacing 2018-09-05 17:46:29 -07:00
Cameron Gutman a7517dace3 Switch to ItemDelegate for PCs and Apps for proper hover and click behavior 2018-09-05 17:08:27 -07:00
Cameron Gutman bd9f2d2009 Remove unused Menu items in ToolButtons 2018-09-05 16:38:52 -07:00
Cameron Gutman 78730f7235 Wake host on a separate worker thread to avoid hanging the UI for DNS resolution 2018-09-05 16:10:32 -07:00
Cameron Gutman 8a2491d955 Populate RC strings for Windows builds 2018-09-05 15:52:50 -07:00
Cameron Gutman 0d68faf40c Remove unused parameter 2018-09-05 15:45:36 -07:00
Cameron Gutman 957a2b2169 Fix box art caching location and cleanup of cache directory by the uninstaller 2018-09-05 15:45:10 -07:00
Cameron Gutman 856318f947 Start the SDL window on the same display as the Qt UI was displayed on 2018-09-05 15:15:53 -07:00
Cameron Gutman 4df9d6b20a Update common-c with FEC fixes 2018-09-05 14:49:07 -07:00
Cameron Gutman 8519d09141 Extract dSYM file for debugging Mac builds 2018-09-04 02:20:00 -07:00
Cameron Gutman 1b4fe83739 Fix AppVeyor symbol artifact name 2018-09-04 01:09:10 -07:00
Cameron Gutman 8fc825b2eb Produce PDBs for Windows builds 2018-09-04 01:03:01 -07:00
Cameron Gutman 6d0fc22ab5 Ensure symbols are always generated for debugging purposes 2018-09-04 00:18:02 -07:00
Cameron Gutman 9a71b4c237 Improve mouse batching to only batch if the last event was less than 1 millisecond ago 2018-09-04 00:09:35 -07:00
Cameron Gutman 9aa487418f Force V-Sync enabled for DXVA2 windowed with DWM composition enabled 2018-09-03 23:34:15 -07:00
Cameron Gutman 203b2fcaca Batch mouse move events for high DPI mice 2018-09-04 00:37:21 -04:00
Cameron Gutman dae21f2638 Detect native panel resolution on Retina displays. Fixes #59 2018-09-04 00:21:37 -04:00
Cameron Gutman af6e99cbac Select the highest full-screen refresh rate that our stream FPS evenly divides 2018-09-03 22:54:41 -04:00
Cameron Gutman 6d6acf4308 Fix release builds 2018-09-03 22:47:31 -04:00
Cameron Gutman 7dd4815edf Allow selection between full-screen exclusive mode and borderless windowed mode 2018-09-03 22:17:34 -04:00
Cameron Gutman 2c068a99a3 Allow FFmpeg renderers to enforce V-sync being enabled or disabled 2018-09-03 20:57:09 -04:00
Cameron Gutman fa93364ddd Ensure the CVDisplayLink is synchronized to the display that our window is on 2018-09-03 20:19:24 -04:00
Cameron Gutman 9b1eef3f63 Pass AV_HWACCEL_FLAG_IGNORE_LEVEL for VDPAU per FFmpeg documentation recommendation 2018-09-03 17:06:21 -04:00
Cameron Gutman ed6c07fd4b Display all frames even if they are corrupt or missing references (as will happen during reference frame invalidation) 2018-09-02 18:45:29 -04:00
Cameron Gutman 5ad96f5287 Display a dialog when the connection is terminated by the remote PC 2018-09-02 18:34:10 -04:00
Cameron Gutman fa8845c7e2 Merge pull request #58 from jhakonen/release-cursor
Release mouse cursor when window focus is lost
2018-09-02 16:51:56 -04:00
Janne Hakonen b96ace7f04 Release mouse cursor when window focus is lost 2018-09-02 00:31:37 +03:00
Cameron Gutman 234a8dc3d5 Fix build with HAVE_SLVIDEO 2018-08-31 10:40:25 -04:00
Cameron Gutman 0467c2f893 Don't rely on Qt 5.10 icon feature 2018-08-31 02:28:26 -04:00
Cameron Gutman 762fe92933 Allow streaming with no audio device. Fixes #52 2018-08-31 00:09:31 -04:00
Cameron Gutman 16d105d462 Avoid hardcoding VCRedist version 2018-08-30 20:47:44 -07:00
Cameron Gutman 8a8b624f47 Add logging for adding a PC and offline/online state transitions 2018-08-30 23:15:06 -04:00
Cameron Gutman 9470615c63 Use QtQuick.Dialogs 1.2 for Qt 5.9 support 2018-08-30 00:00:05 -04:00
Cameron Gutman 8f0ea700a5 Fix QML error when pairing successfully 2018-08-29 23:59:19 -04:00
Cameron Gutman b7cbec0b1c Pass RFI flags to decoder 2018-08-25 13:36:54 -07:00
Cameron Gutman 9be9934b8c Add reference frame invalidation for the software decoder 2018-08-25 12:38:04 -07:00
Cameron Gutman 55f0e1e1d5 Use QT_OPENGL to set ANGLE 2018-08-25 11:59:32 -07:00
Cameron Gutman bbd784fc85 Use ANGLE for rendering on Windows 2018-08-25 11:35:55 -07:00
Cameron Gutman 408f93732b Fix 'invalid VADisplay' errors on renderer destruction 2018-08-25 11:21:52 -07:00
Cameron Gutman fb53c95207 Version 0.3.0 2018-08-21 01:18:55 -07:00
Cameron Gutman 960e083152 Add manifest for Windows 2018-08-21 01:07:30 -07:00
Cameron Gutman 7e560aa09e Use MMCSS when streaming with the DXVA renderer 2018-08-21 00:53:41 -07:00
Cameron Gutman 7d9478b774 Fix FlipEx on Windows 7 2018-08-21 00:49:29 -07:00
Cameron Gutman 0fa0ab43c9 Fix broken Alt+Tab behavior on Windows 2018-08-20 22:48:48 -07:00
Cameron Gutman f9cdd49d2e Add settings option to disable V-sync for lowest possible latency on Windows 2018-08-20 22:25:19 -07:00
Cameron Gutman 7661f5a3d7 Use SetThreadExecutionState() to disable the screensaver since SDL doesn't currently call that. Fixes #46 2018-08-20 21:36:23 -07:00
Cameron Gutman 395f337bbb Switch full-screen mode back to full-screen exclusive on Windows to reduce latency by 1 frame 2018-08-20 21:19:00 -07:00
Cameron Gutman 9e9fe2a2cf Work around DWM bug on Windows 7 causing horrible performance 2018-08-20 21:17:25 -07:00
Cameron Gutman 5bf88cfe96 Fix Mac build 2018-08-20 18:57:11 -07:00
Cameron Gutman f7d3c10c9d Plumb the ability to disable V-sync through the video renderers 2018-08-20 18:19:42 -07:00
Cameron Gutman 6b395c816f Allow Pacer to wait for frames up until a few ms before v-sync for better smoothness and lower latency 2018-08-20 17:53:35 -07:00
Cameron Gutman d6e7173af0 Add a VsyncSource for renderers that already block for V-sync 2018-08-20 17:24:47 -07:00
Cameron Gutman 1a60484abc Use hardware vertex processing if available 2018-08-19 22:45:17 -07:00
Cameron Gutman 62f765b2b4 Switch DXVA2 to IDirect3D9Ex APIs for more detailed control of rendering 2018-08-19 21:53:39 -07:00
Cameron Gutman 57f2fb07f0 Speed up build times by allow dependencies to be built in parallel 2018-08-19 14:33:37 -07:00
Cameron Gutman 1b8c3aae73 Speed up resize/full-screen by avoiding excessive decoder resets 2018-08-19 01:19:23 -07:00
Cameron Gutman 845e84adb7 Avoid the test frame for DXVA2 and VT APIs to address flickering in full-screen on Win7 2018-08-19 00:59:04 -07:00
Cameron Gutman 91c0429e2c Remove cache directories on uninstall 2018-08-18 15:43:24 -07:00
Cameron Gutman 30f673efe1 Create Qt components with all parameters specified otherwise it will be instantiated with default properties 2018-08-16 23:29:46 -07:00
Cameron Gutman c1b1719914 Recreate the VT decoder if the display layer fails. Minor fix to Path class. 2018-08-16 23:09:40 -07:00
Cameron Gutman 9fbdd794b9 Improve support for full-screen exclusive mode 2018-08-16 22:25:14 -07:00
Cameron Gutman 38ff2bf5cb Store all files in the current directory for portable installations. Fixes #43 2018-08-16 21:04:47 -07:00
Cameron Gutman 345e800abd Always submit frames to Pacer if it's using a frame queue 2018-08-16 17:59:33 -07:00
Cameron Gutman 404eaa44e4 Implement frame pacing for Windows using D3DKMTWaitForVerticalBlankEvent 2018-08-16 01:29:53 -07:00
Cameron Gutman 8e257ab241 Remove unused function 2018-08-16 00:04:28 -07:00
Cameron Gutman fabbad5370 Fix build error on Windows and warnings 2018-08-16 00:02:43 -07:00
Cameron Gutman 7d61acb2a8 Use Pacer to drive all rendering 2018-08-15 23:57:03 -07:00
Cameron Gutman f6a451d6e7 Add PoC VsyncSource for Windows using DwmFlush(). Soon to be replaced with D3DKMTWaitForVerticalBlankEvent(). 2018-08-15 23:20:56 -07:00
Cameron Gutman e68a15c825 Move the Vsync logic from VTRenderer into a VsyncSource 2018-08-15 22:02:15 -07:00
Cameron Gutman f929cffce7 Let Pacer know what the display Hz and stream FPS are so it can make better drop decisions 2018-08-15 21:10:35 -07:00
Cameron Gutman f171588616 Pass the maximum frame rate into FFmpeg renderers 2018-08-15 20:41:19 -07:00
Cameron Gutman e467ce7ce8 Uninitialize video subsystem before allowing a racing session to start to prevent an assert from firing 2018-08-15 20:35:11 -07:00
Cameron Gutman 1fda7550da Sign the MSI within the bundle too 2018-08-15 20:29:59 -07:00
Cameron Gutman f714a5d0cb Refactor frame pacing from VT renderer into a separate class for sharing with other renderers 2018-08-14 19:13:17 -07:00
Cameron Gutman 2a3d5a27a8 Add issue templates 2018-08-14 18:03:10 -07:00
Cameron Gutman c9bf5ae79d Prevent underflow during audio frame drop calculation 2018-08-14 17:39:29 -07:00
Cameron Gutman cf2d8f71d6 Avoid setting timer resolution to 1 ms while Moonlight is not streaming 2018-08-13 22:23:05 -07:00
Cameron Gutman 611d9ac1ba Pass the chroma type to the VDPAU video mixer 2018-08-13 20:02:55 -07:00
Cameron Gutman f3309087ab Avoid polling if the Qt window is minimized 2018-08-13 19:47:42 -07:00
Cameron Gutman 860d317330 Version 0.2.0 2018-08-12 01:42:28 -07:00
Cameron Gutman bab11a731e Fix uninitialized gamepad mask in multi-controller mode 2018-08-12 01:39:22 -07:00
Cameron Gutman df9e7bead0 Add a hack to the installer to workaround the ffmpeg versioning issue. Fixes #27 2018-08-12 01:23:15 -07:00
Cameron Gutman 962dc847e7 Enable stale and no-response bots 2018-08-12 00:21:07 -07:00
Cameron Gutman 566181e8d7 Display error if video decoder doesn't initialize 2018-08-11 22:49:36 -07:00
Cameron Gutman 5f5f835b2e Display a warning when running under Wayland 2018-08-11 22:42:25 -07:00
Cameron Gutman c0e697eb58 Add packages required for SDL Wayland compilation to Travis CI config 2018-08-11 20:56:08 -07:00
Cameron Gutman d3c2f50b20 Remove custom VAAPI logging functions 2018-08-11 20:32:28 -07:00
Cameron Gutman f1ee1eeb63 Avoid using window systems that SDL wasn't compiled to support 2018-08-11 20:19:20 -07:00
Cameron Gutman bb18519e44 WIP Wayland rendering code for VAAPI 2018-08-11 19:43:36 -07:00
Cameron Gutman 8e2ad133ea Add more details on build environment setup 2018-08-11 17:50:29 -07:00
Cameron Gutman 5e82e8413f Use create-dmg to get a much nicer looking DMG that's also code-signed 2018-08-11 14:52:44 -07:00
Cameron Gutman 917f6e5ee9 Only capture the mouse by default for full screen streaming 2018-08-11 14:19:42 -07:00
Cameron Gutman e1ca6e644e Create Windows portable zip for non-admin users 2018-08-11 14:12:55 -07:00
Cameron Gutman f1e89242cb Handle EINTR to fix streaming with GDB attached 2018-08-10 15:53:27 -07:00
Cameron Gutman 91145cde0f Bump minimum macOS version to 10.11 in line with Qt 5.11 2018-08-10 00:08:56 -07:00
Cameron Gutman 8ae01fae3f Place logs in /tmp on macOS 2018-08-09 23:54:44 -07:00
Cameron Gutman 4e60ccb2ff Add tooltips for toolbar buttons 2018-08-09 23:20:20 -07:00
Cameron Gutman 186052f21d Fix update button color 2018-08-09 23:18:07 -07:00
Cameron Gutman 0fb3bb0727 Add automatic update checking. Fixes #8 2018-08-09 22:51:27 -07:00
Cameron Gutman 8f61a11452 Remove improper cast to unsigned value 2018-08-09 20:21:18 -07:00
Cameron Gutman 4b1d3b0125 Warn users when their configuration is impossible to satisfy. Fixes #26 2018-08-09 19:37:49 -07:00
Cameron Gutman 3f81d055af Return to the PC grid if the selected PC goes offline or becomes unpaired 2018-08-09 18:48:40 -07:00
Cameron Gutman c61182bcb2 Allow FFmpeg decoder to self-heal in cases of UAC dialogs and other unexpected loss of hardware state 2018-08-09 18:39:38 -07:00
Cameron Gutman 2fdfb3e998 Fix a few incorrectly mapped keys. Fixes #28 2018-08-06 20:30:09 -07:00
Cameron Gutman 9beb51ecac Version 0.1.0 2018-08-05 19:36:30 -07:00
Cameron Gutman 7be167856e Only enable game optimizations when streaming at officially supported resolutions 2018-08-05 18:09:35 -07:00
Cameron Gutman e1d68ef097 Fix Y values wrapping at -32768 2018-08-05 18:01:50 -07:00
Cameron Gutman 5ec256a734 Fix QML warning 2018-08-05 17:09:29 -07:00
Cameron Gutman e4d7d190e2 Suppress logging for applist queries 2018-08-05 17:03:05 -07:00
Cameron Gutman b6763bb9f0 Fix VDPAU rendering at 4K 2018-08-05 16:33:08 -07:00
Cameron Gutman 6d9c0c96c9 Fix streaming with non-aligned resolutions with DXVA2 on my Quadro P400 2018-08-05 15:25:40 -07:00
Cameron Gutman 73dd5630df Fix FPS setting restore logic 2018-08-05 15:09:36 -07:00
Cameron Gutman b76645e26b Fix resolution comparision issue with portrait displays 2018-08-05 15:03:24 -07:00
Cameron Gutman 9cc20c27ab Add support for native resolution streaming 2018-08-05 14:55:26 -07:00
Cameron Gutman 36b42f6e50 Suppress log spam from serverinfo queries 2018-08-05 14:06:42 -07:00
Cameron Gutman 45ebf2ca7d Add log messages for server GPU, bitrate, and slices 2018-08-05 13:32:04 -07:00
Cameron Gutman 2140c94cd1 Fix status icon centering on PC view. Fixes #10 2018-08-05 12:47:08 -07:00
Cameron Gutman 470769e910 Replace running status text with app icon overlay 2018-08-05 12:19:54 -07:00
Cameron Gutman 493165f57f Add a busy indicator while waiting for initial contact with a PC 2018-08-05 12:13:08 -07:00
Cameron Gutman 1dcf55515b Initial replacement of status text with status icons on the PC view 2018-08-05 11:47:14 -07:00
Cameron Gutman b04bc5117d Fix scaling logic in the Linux renderers using the new helper function 2018-08-04 22:31:14 -07:00
Cameron Gutman 5cbb38091b Create new utility function for scaling with aspect ratio and use it for DXVA2 2018-08-04 22:22:15 -07:00
Cameron Gutman b076744f00 Support HEVC software decoding and forcing HEVC without GPU support 2018-08-04 16:45:31 -07:00
Cameron Gutman 723e815546 Add warning for forcing software decoding 2018-08-04 16:15:13 -07:00
Cameron Gutman ca72cbb135 Implement displaying launch warnings. Fixes #12 2018-08-04 16:05:37 -07:00
Cameron Gutman 1e8c2b179a Only fill the screen in windowed mode if the stream is larger than usable display area 2018-08-04 15:34:56 -07:00
Cameron Gutman f2ed3a8502 Make stream window resizable 2018-08-04 15:34:56 -07:00
Cameron Gutman 2bcc15e1d2 Rebuild FFmpeg for Windows to reduce install size 2018-08-04 02:12:30 -07:00
Cameron Gutman 4935baa72a Rebuild FFmpeg libraries for Mac to reduce package size 2018-08-04 01:11:19 -07:00
Cameron Gutman 10cfaa2a5f Fix Linux CI 2018-08-03 18:59:37 -07:00
Cameron Gutman 3117e360f4 Retry surface creation to work around transient VDP_STATUS_RESOURCES errors 2018-08-03 18:26:55 -07:00
Cameron Gutman 48a770edec Enable HEVC support on NVIDIA GPUs with VDPAU 2018-08-03 02:49:43 -07:00
Cameron Gutman fca402138f Add VDPAU renderer 2018-08-03 02:11:44 -07:00
Cameron Gutman 2853e498b0 Minor debugging improvements 2018-08-02 23:24:44 -07:00
Cameron Gutman 509dba2264 Enable ffmpeg logging during codec initialization 2018-08-02 22:48:40 -07:00
Cameron Gutman b8dd131af8 Allow gamepad input when the app doesn't have focus. Fixes #23 2018-08-02 22:36:44 -07:00
Cameron Gutman 0c18bcdd5e Fix DXVA 2 regression on Windows 2018-08-02 22:28:59 -07:00
Cameron Gutman 624578f286 Fix decoder reinitialization after test frame 2018-08-02 21:57:16 -07:00
Cameron Gutman c18e99c9ec Validate that the codec is functional before using it. Fixes streaming failure with VAAPI if hardware lacks codec support 2018-08-02 21:37:46 -07:00
Cameron Gutman 979ba2dab7 Version 0.0.5 2018-08-01 23:00:55 -07:00
Cameron Gutman 56277af5d1 Fix quit error message when another PC started the streaming session 2018-08-01 22:43:20 -07:00
Cameron Gutman 608db6a3e5 Implement support for quitting apps 2018-08-01 22:32:21 -07:00
Cameron Gutman be52272e5c Fix state corruption caused by having 2 separate ComputerModels. Fixes #6 2018-08-01 21:29:03 -07:00
Cameron Gutman 32f305347f Bundle ANGLE libraries into the installer to load on hardware without good OpenGL support 2018-08-01 20:40:05 -07:00
Cameron Gutman 18dd6b09bb Remove 10 ms sleep hidden inside SDL_WaitEvent(). This fixes mouse polling rate being capped at 100 Hz (1 second / 10 ms) and the rendering FPS being capped at 90 FPS. 2018-08-01 18:26:50 -07:00
Cameron Gutman 3ed5f9edf7 Implement C++ functions for quitting apps and querying running apps 2018-07-31 22:21:39 -07:00
Cameron Gutman 017362a5d1 Only allow waking PCs that are not online and paired 2018-07-31 22:10:38 -07:00
Cameron Gutman e250e08242 Fix AMD vendor ID 2018-07-31 18:22:10 -07:00
Cameron Gutman 48d5a7fb51 Add code signing for Windows. Fixes #11 2018-07-31 18:16:39 -07:00
Cameron Gutman 141ee11e2f Avoid HEVC on GPUs using hybrid decoding. Fixes #14 2018-07-31 18:09:09 -07:00
Cameron Gutman 4e82ea298e Update AppStream metadata for 0.0.4a 2018-07-30 23:42:18 -07:00
Cameron Gutman 22be912788 Fix accent prompt appearing when keys are held down on macOS 2018-07-30 22:44:19 -07:00
Cameron Gutman 02242b9b34 Replace Opus submodule with prebuilt libopus 1.2.1 binaries for Windows and Mac 2018-07-30 22:10:08 -07:00
Cameron Gutman e9c1f883da Use full-screen desktop mode for Mac to allow Spaces to work 2018-07-30 00:03:17 -07:00
Cameron Gutman 141f6669e8 Version 0.0.4 2018-07-29 21:44:10 -07:00
Cameron Gutman 6300d242cc Update AppStream metadata 2018-07-29 21:40:34 -07:00
Cameron Gutman 186579266c Fix Online -> Offline computer transition 2018-07-29 19:08:29 -07:00
Cameron Gutman 13490af90c Replace OpenSSL dylibs with static build for macOS 10.10+ 2018-07-29 19:07:42 -07:00
R. Aidan Campbell 23a9c2cd9e context menu opened by default for offline hosts 2018-07-29 16:13:42 -07:00
R. Aidan Campbell 88631e4a63 Merge branch 'master' of github.com:cgutman/moonlight-qt 2018-07-29 16:04:53 -07:00
R. Aidan Campbell 9e744ce169 Now with WoL and PC deletion from QML. Deletion needs bugfix, possibly not committed to storage? 2018-07-29 16:04:45 -07:00
Cameron Gutman 77a68db8c1 Code signing for Mac 2018-07-29 15:35:51 -07:00
Cameron Gutman 27247f253c Add Travis CI badge 2018-07-29 13:50:11 -07:00
Cameron Gutman ada7f92c75 Travis CI for Mac 2018-07-29 13:38:05 -07:00
Cameron Gutman 87db4e88a5 Replace autogenerate app bundle strings with real ones and relocate the icon for Mac 2018-07-29 13:05:18 -07:00
R. Aidan Campbell 277594139b Updated macos .app icon to use proper logo 2018-07-29 10:21:53 -07:00
Cameron Gutman 79d5e35306 Mac deployment
- Use the SDL2 framework wholesale instead of manually extracting dylib and headers
- Run install_name_tool on the remaining dylibs so they use the correct load path
- Add generate-dmg.sh for creating packaged Mac builds
2018-07-29 03:38:59 -07:00
Cameron Gutman cda80b319e Publish artifacts from AppVeyor builds 2018-07-28 23:59:20 -07:00
Cameron Gutman ab4b024df4 Travis CI support 2018-07-28 22:49:36 -07:00
Cameron Gutman 7f9cbac232 Fix more AppVeyor bugs 2018-07-28 19:56:08 -07:00
Cameron Gutman 8470ffcb0d Fix some build script bugs uncovered by AppVeyor 2018-07-28 19:44:09 -07:00
Cameron Gutman 92c897762b Fix AppVeyor link 2018-07-28 19:33:11 -07:00
Cameron Gutman 36014cfcfd Add AppVeyor build status 2018-07-28 19:29:54 -07:00
Cameron Gutman df26f731eb Improve debugging messages 2018-07-28 16:06:26 -07:00
Cameron Gutman e39b5a6a72 Block x86 installs on x64 OSes 2018-07-28 13:59:27 -07:00
Cameron Gutman c7138fe36c Fix Discord link 2018-07-28 03:52:24 -07:00
Cameron Gutman b5deab772d Add Discord link 2018-07-28 03:47:34 -07:00
Cameron Gutman 4bfeccec74 Update README with more information 2018-07-28 03:45:45 -07:00
Cameron Gutman 42ed39a67f Version 0.0.3 2018-07-28 03:13:39 -07:00
Cameron Gutman 68612db86d Fix audio glitching on GFE 3.14.1 at low bitrates 2018-07-28 03:13:05 -07:00
Cameron Gutman 3ad5f1478c Comment out HDR option since it's non-working and tweak the remaining option text 2018-07-28 03:06:11 -07:00
Cameron Gutman a81b501d7d Hide the gamepad mapping button since it's unimplemented 2018-07-28 01:27:42 -07:00
Cameron Gutman 871988b2cb Add 1440p and up to 90 FPS streaming options for > 60 Hz monitors 2018-07-28 01:22:31 -07:00
Cameron Gutman 59d087adf5 Add function for querying maximum supported streaming frame rate 2018-07-28 00:25:52 -07:00
Cameron Gutman d9ed3f9694 Fix incorrect key mappings with non-QWERTY keyboards on both host and client 2018-07-27 02:08:54 -07:00
Cameron Gutman e652dbcc79 Version 0.0.2 2018-07-27 00:14:29 -07:00
Cameron Gutman b5965d763c Create desktop shortcut, detect various error conditions to fail installation, and only install VCRedist if it's not already present 2018-07-27 00:13:02 -07:00
Cameron Gutman 717c3903d1 Fix top padding for grid views 2018-07-26 22:47:48 -07:00
Cameron Gutman 5854ce2000 Fix handling of long app names 2018-07-26 22:39:45 -07:00
Cameron Gutman eb1316ce32 Avoid asserting if we get a box art load completion callback for an app that's gone 2018-07-26 22:16:27 -07:00
Cameron Gutman c31e7262b2 Fix decoding of non-ASCII characters 2018-07-26 22:15:52 -07:00
Cameron Gutman 9e6aadffa1 Populate the DXVA2_VideoDesc with format details from the AVFrame 2018-07-26 21:04:05 -07:00
Cameron Gutman 0bde932550 Prevent HEVC streaming with the software decoder for performance reasons 2018-07-26 19:26:22 -07:00
Cameron Gutman f5499be215 Warn when no hardware decoding is available 2018-07-26 12:13:38 -07:00
Cameron Gutman 42988e0dbf Limit log file sizes to 10k lines 2018-07-26 00:21:26 -07:00
Cameron Gutman b16aac0c06 Fix priority and formatting on many debug messages 2018-07-25 23:47:32 -07:00
Cameron Gutman 7c74cdc50b Add enhanced logging support, including log to file for release builds 2018-07-25 23:25:17 -07:00
Cameron Gutman 26ee8564f8 Include VC redistributable in the bundle 2018-07-25 19:52:58 -07:00
Cameron Gutman 718a575478 Reorganize WiX sources 2018-07-25 19:33:09 -07:00
Cameron Gutman 1f314b839e Various installer fixes 2018-07-25 19:16:06 -07:00
Cameron Gutman 34494e4c89 Add initial WiX installer for Windows 2018-07-25 02:53:51 -07:00
Cameron Gutman 03dc563b32 Create Moonlight png for WiX bootstrapper 2018-07-24 21:11:48 -07:00
Cameron Gutman 5e4182f0a0 Add Moonlight.ico for Windows and script to generate it 2018-07-24 20:07:10 -07:00
Cameron Gutman 010475e8a2 Use per-display undefined window positions 2018-07-22 18:40:15 -07:00
Cameron Gutman bd170c4bd6 Always use the current display mode and just let the renderers scale the output 2018-07-22 18:28:17 -07:00
Cameron Gutman 386e463942 Also use the SDL icon for Linux 2018-07-22 17:42:31 -07:00
Cameron Gutman 9d840afdfb Add an SDL window icon for Windows 2018-07-22 17:07:45 -07:00
Cameron Gutman 83b81aad4b Don't capture the mouse initially on debug builds 2018-07-22 16:21:15 -07:00
Cameron Gutman 4caa7ec749 Enable LC_DEBUG for debug builds 2018-07-22 16:05:42 -07:00
Cameron Gutman cf3cbd627d Use full-screen windowed mode to avoid having to enable V-Sync on DXVA2 2018-07-22 15:42:17 -07:00
Cameron Gutman 3bb62a82e1 Avoid doing a display mode change on Linux when entering full-screen 2018-07-22 14:03:47 -07:00
Cameron Gutman a06a625d48 Rework the logo SVG to look better as an app icon, by Jorys Paulin 2018-07-22 03:21:19 -07:00
Cameron Gutman 13d686b6f8 Rename output binary to moonlight 2018-07-22 03:16:53 -07:00
Cameron Gutman 75c7b2828f Remove unneccessary FFmpeg files 2018-07-22 02:47:59 -07:00
Cameron Gutman 18c5e19ac9 Add files for Flatpak deployment 2018-07-22 01:51:05 -07:00
Cameron Gutman 06635d9f9a Update common-c submodule 2018-07-21 20:30:53 -07:00
Cameron Gutman ad57a55d12 Add (untested) Steam Link video decoding/rendering 2018-07-21 20:29:05 -07:00
Cameron Gutman a97e4babf9 Fix build on GCC 4.x and lower 2018-07-21 19:34:13 -07:00
Cameron Gutman 1117c49867 Fix another brown-paper-bag bug in the build system 2018-07-21 19:26:24 -07:00
Cameron Gutman dc723a8da1 Fix strict aliasing violation 2018-07-21 19:16:08 -07:00
Cameron Gutman 27239ff320 Fix typo in ARM configuration 2018-07-21 19:15:16 -07:00
Cameron Gutman b073c90f0f Fix build on OpenSSL 1.0.1 2018-07-21 19:02:12 -07:00
Cameron Gutman 519626a6d5 Fix compilation with Qt 5.9 2018-07-21 18:47:41 -07:00
Cameron Gutman 71c11cacd9 Update opus project for ARM 2018-07-21 18:30:12 -07:00
Cameron Gutman b7b452875d Add a missing HAVE_FFMPEG guard 2018-07-21 17:32:00 -07:00
Cameron Gutman eb536c2273 Overhaul platform and decoder/renderer detection 2018-07-21 17:00:09 -07:00
R. Aidan Campbell c816a6198c fix macos build: for macos, Q_OS_UNIX is also defined 2018-07-21 14:31:37 -07:00
Cameron Gutman 399154f5f5 Implement VAAPI rendering for X11 2018-07-21 00:16:03 -07:00
Cameron Gutman 64a839c2f9 Create the window after starting streaming to avoid a mouse capture bug in SDL on Windows 2018-07-20 19:55:07 -07:00
Cameron Gutman 20288210be Fix VT video centering after transition from windowed to full screen 2018-07-20 19:37:54 -07:00
Cameron Gutman 7b235743cb Count the window chrome size in our placement decision and fix sizing after full-screen toggle 2018-07-20 19:18:55 -07:00
Cameron Gutman 4c507f1179 Move SDL subsystem init into appropriate components to fix gamepad detection issue 2018-07-20 18:15:46 -07:00
Cameron Gutman e516861366 Set window dimensions properly when toggling full-screen 2018-07-20 16:01:22 -07:00
R. Aidan Campbell 48c2a8c5b0 Merge branch 'master' of github.com:cgutman/moonlight-qt 2018-07-20 15:47:57 -07:00
R. Aidan Campbell b41e8c07d2 Switched settings page to two-pane layout 2018-07-20 15:47:50 -07:00
Cameron Gutman 10b5245919 Reset the decoder when D3D state is lost 2018-07-20 15:31:57 -07:00
R. Aidan Campbell 86874a2e1c minor wording change in settings 2018-07-20 14:36:39 -07:00
R. Aidan Campbell f951a01a68 changed audio configuration to a dropdown, allowing auto/stereo/surround 2018-07-20 14:35:47 -07:00
R. Aidan Campbell d12fed3e7f remove unused settings 2018-07-20 14:24:27 -07:00
R. Aidan Campbell 4566139c75 now with video codec selection 2018-07-20 14:22:22 -07:00
R. Aidan Campbell 38df63b962 now with video decoder selection 2018-07-20 14:14:38 -07:00
Cameron Gutman faaf9dfa07 Fix crashes when restarting streaming with DXVA2 renderer due to leaked surface references 2018-07-20 13:10:54 -07:00
Cameron Gutman 9c2befc386 Only require OpenSSL 1.1 on Apple platforms where SecureTransport requires keys from PEM_write_bio_PrivateKey_traditional() 2018-07-19 23:19:00 -07:00
Cameron Gutman e85ca2a6c9 Reduce VideoToolbox renderer video latency 2018-07-18 22:25:46 -07:00
Cameron Gutman e32aace443 Draw a black background behind the rendering 2018-07-18 22:07:43 -07:00
Cameron Gutman bd8e4a4bce Fix DXVA video centering 2018-07-18 21:48:46 -07:00
Cameron Gutman 5ee1358712 Implement a proper destructor for ComputerManager to prevent polling threads from living beyond QGuiApplication 2018-07-18 21:27:43 -07:00
Cameron Gutman 06501bd4b7 Fix video scaling on DXVA2 2018-07-18 19:10:14 -07:00
Cameron Gutman a4ad58d9ec Fix some Clang static analysis warnings 2018-07-18 18:39:33 -07:00
Cameron Gutman 76d99cd60e Raise combo keys for all special combos, not just quit 2018-07-18 00:14:12 -07:00
Cameron Gutman 1b85dcc829 Improve frame pacing on VideoToolbox renderer 2018-07-18 00:11:17 -07:00
Cameron Gutman c97e298b80 Use the queueFrame() helper 2018-07-17 20:05:59 -07:00
271 changed files with 14707 additions and 16739 deletions

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us address broken functionality
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to reproduce**
Any special steps that are required for the bug to appear.
**Screenshots**
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
**Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults?
- If so, which settings have been changed?
- Does the problem still occur after reverting settings back to default?
**Client PC details (please complete the following information)**
- OS: [e.g. Windows 10 1803]
- Moonlight Version: [e.g. v0.2.0]
**Logs (please attach)**
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
**Additional context**
Add any other context about the problem here.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+8
View File
@@ -0,0 +1,8 @@
# ProBot No Response (https://probot.github.io/apps/no-response/)
daysUntilClose: 7
responseRequiredLabel: 'need more info'
closeComment: >
This issue has been automatically closed because there was no response to a
request for more information from the issue opener. Please leave a comment or
open a new issue if you have additional information related to this issue.
+14
View File
@@ -0,0 +1,14 @@
# ProBot Stale (https://probot.github.io/apps/stale/)
daysUntilStale: 90
daysUntilClose: 7
exemptLabels:
- accepted
- bug
- enhancement
- meta
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
closeComment: false
+4
View File
@@ -1 +1,5 @@
*pro.user *pro.user
**/.vs/
build/
-3
View File
@@ -1,9 +1,6 @@
[submodule "moonlight-common-c/moonlight-common-c"] [submodule "moonlight-common-c/moonlight-common-c"]
path = moonlight-common-c/moonlight-common-c path = moonlight-common-c/moonlight-common-c
url = https://github.com/moonlight-stream/moonlight-common-c.git url = https://github.com/moonlight-stream/moonlight-common-c.git
[submodule "opus/opus"]
path = opus/opus
url = https://github.com/xiph/opus.git
[submodule "qmdnsengine/qmdnsengine"] [submodule "qmdnsengine/qmdnsengine"]
path = qmdnsengine/qmdnsengine path = qmdnsengine/qmdnsengine
url = https://github.com/cgutman/qmdnsengine.git url = https://github.com/cgutman/qmdnsengine.git
+50
View File
@@ -0,0 +1,50 @@
language: cpp
git:
depth: 1
matrix:
include:
- os: osx
compiler: clang
- os: linux
sudo: required
dist: xenial
compiler: gcc
addons:
apt:
sources:
- sourceline: 'ppa:beineri/opt-qt596-xenial'
- sourceline: 'ppa:jonathonf/ffmpeg-4'
packages:
- qt59base
- qt59quickcontrols2
- qt59svg
- qt59multimedia
- libesd0-dev
- libgl-dev
- libegl1-mesa-dev
- libxkbcommon-dev
- wayland-protocols
- libopus-dev
- libavcodec-dev
- libavutil-dev
- libva-dev
- libvdpau-dev
install:
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
before_script:
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
- qmake -v
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
script:
- qmake moonlight-qt.pro -spec $QMAKESPEC
- make debug
- make release
+21 -7
View File
@@ -1,20 +1,34 @@
# Moonlight PC # Moonlight PC
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. [Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios). Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
This is a work-in-progress port and is only suitable for development use at this point. See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
[![Windows AppVeyor Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
[![Mac and Linux Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-qt.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-qt)
## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
- 5.1 surround sound audio
- HEVC support for better image quality at reduced bandwidth
- Keyboard and mouse support
- Gamepad support with SDL gamepad mappings
## Building ## Building
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download 1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
3. Run `git submodule update --init --recursive` from within `moonlight-qt/` - On Windows, install Visual Studio 2017 and select MSVC 2017 toolchain during Qt installation
4. Open the project in Qt Creator or build from qmake on the command line - On Mac, install the latest version of XCode
- On Linux, install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel`
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
3. Open the project in Qt Creator or build from qmake on the command line
## Contribute ## Contribute
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
1. Fork us 1. Fork us
2. Write code 2. Write code
3. Send Pull Requests 3. Send Pull Requests
+13 -7
View File
@@ -3,24 +3,30 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>moonlight-qt</string> <string>Moonlight</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Created by Qt/QMake</string> <string>Stream games from your NVIDIA GameStream-enabled PC</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>macos</string> <string>moonlight</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.yourcompany.moonlight-qt</string> <string>com.moonlight-stream.Moonlight</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>10.10</string> <string>10.11</string>
<key>NOTE</key>
<string>This file was generated by Qt/QMake.</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
<true/> <true/>
<key>CFBundleVersion</key>
<string>0.5.1</string>
<key>CFBundleShortVersionString</key>
<string>0.5.1</string>
<key>CFBundleDisplayName</key>
<string>Moonlight</string>
</dict> </dict>
</plist> </plist>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<description>Moonlight Game Streaming</description>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitor</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
</application>
</compatibility>
</assembly>
+167 -37
View File
@@ -1,7 +1,19 @@
QT += core quick network quickcontrols2 QT += core quick network quickcontrols2 svg multimedia
CONFIG += c++11 CONFIG += c++11
TARGET = moonlight-qt unix:!macx {
TARGET = moonlight
} else {
# On macOS, this is the name displayed in the global menu bar
TARGET = Moonlight
}
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
TEMPLATE = app TEMPLATE = app
# The following define makes your compiler emit warnings if you use # The following define makes your compiler emit warnings if you use
@@ -25,23 +37,44 @@ win32 {
LIBS += -L$$PWD/../libs/windows/lib/x64 LIBS += -L$$PWD/../libs/windows/lib/x64
} }
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
} }
macx { macx {
INCLUDEPATH += $$PWD/../libs/mac/include INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
LIBS += -L$$PWD/../libs/mac/lib LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
} }
unix:!macx { unix:!macx {
CONFIG += link_pkgconfig CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2 libavcodec libavdevice libavformat libavutil PKGCONFIG += openssl sdl2 opus
packagesExist(libavcodec) {
PKGCONFIG += libavcodec libavutil
CONFIG += ffmpeg
packagesExist(libva) {
packagesExist(libva-x11) {
CONFIG += libva-x11
}
packagesExist(libva-wayland) {
CONFIG += libva-wayland
}
CONFIG += libva
}
packagesExist(vdpau) {
CONFIG += libvdpau
}
}
} }
win32 { win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavdevice -lavformat -lavutil LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
CONFIG += ffmpeg
} }
macx { macx {
LIBS += -lssl -lcrypto -lSDL2 -lavcodec.58 -lavdevice.58 -lavformat.58 -lavutil.56 LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework AppKit LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
CONFIG += ffmpeg
} }
SOURCES += \ SOURCES += \
@@ -54,18 +87,14 @@ SOURCES += \
settings/streamingpreferences.cpp \ settings/streamingpreferences.cpp \
streaming/input.cpp \ streaming/input.cpp \
streaming/session.cpp \ streaming/session.cpp \
streaming/audio.cpp \ streaming/audio/audio.cpp \
streaming/video/ffmpeg.cpp \ streaming/audio/renderers/sdlaud.cpp \
streaming/audio/renderers/qtaud.cpp \
gui/computermodel.cpp \ gui/computermodel.cpp \
gui/appmodel.cpp \ gui/appmodel.cpp \
streaming/video/ffmpeg-renderers/sdl.cpp streaming/streamutils.cpp \
backend/autoupdatechecker.cpp \
win32 { path.cpp
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
}
macx {
SOURCES += streaming/video/avfoundation.mm
}
HEADERS += \ HEADERS += \
utils.h \ utils.h \
@@ -77,17 +106,90 @@ HEADERS += \
settings/streamingpreferences.h \ settings/streamingpreferences.h \
streaming/input.hpp \ streaming/input.hpp \
streaming/session.hpp \ streaming/session.hpp \
streaming/audio/renderers/renderer.h \
streaming/audio/renderers/sdl.h \
streaming/audio/renderers/qtaud.h \
gui/computermodel.h \ gui/computermodel.h \
gui/appmodel.h \ gui/appmodel.h \
streaming/video/decoder.h \ streaming/video/decoder.h \
streaming/video/ffmpeg.h \ streaming/streamutils.h \
streaming/video/ffmpeg-renderers/renderer.h backend/autoupdatechecker.h \
path.h
# Platform-specific renderers and decoders
ffmpeg {
message(FFmpeg decoder selected)
DEFINES += HAVE_FFMPEG
SOURCES += \
streaming/video/ffmpeg.cpp \
streaming/video/ffmpeg-renderers/sdlvid.cpp \
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
HEADERS += \
streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
}
libva {
message(VAAPI renderer selected)
PKGCONFIG += libva
DEFINES += HAVE_LIBVA
SOURCES += streaming/video/ffmpeg-renderers/vaapi.cpp
HEADERS += streaming/video/ffmpeg-renderers/vaapi.h
}
libva-x11 {
message(VAAPI X11 support enabled)
PKGCONFIG += libva-x11
DEFINES += HAVE_LIBVA_X11
}
libva-wayland {
message(VAAPI Wayland support enabled)
PKGCONFIG += libva-wayland
DEFINES += HAVE_LIBVA_WAYLAND
}
libvdpau {
message(VDPAU renderer selected)
DEFINES += HAVE_LIBVDPAU
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
}
config_SLVideo {
message(SLVideo decoder/renderer selected)
DEFINES += HAVE_SLVIDEO
LIBS += -lSLVideo
SOURCES += streaming/video/sl.cpp
HEADERS += streaming/video/sl.h
}
win32 { win32 {
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h message(DXVA2 renderer selected)
SOURCES += \
streaming/video/ffmpeg-renderers/dxva2.cpp \
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp
HEADERS += \
streaming/video/ffmpeg-renderers/dxva2.h \
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
} }
macx { macx {
HEADERS += streaming/video/avfoundation.h message(VideoToolbox renderer selected)
SOURCES += \
streaming/video/ffmpeg-renderers/vt.mm \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
HEADERS += \
streaming/video/ffmpeg-renderers/vt.h \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
} }
RESOURCES += \ RESOURCES += \
@@ -107,13 +209,6 @@ else:unix: LIBS += -L$$OUT_PWD/../moonlight-common-c/ -lmoonlight-common-c
INCLUDEPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src INCLUDEPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
DEPENDPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src DEPENDPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../opus/release/ -lopus
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../opus/debug/ -lopus
else:unix: LIBS += -L$$OUT_PWD/../opus/ -lopus
INCLUDEPATH += $$PWD/../opus/opus/include
DEPENDPATH += $$PWD/../opus/opus/include
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/release/ -lqmdnsengine win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/release/ -lqmdnsengine
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/debug/ -lqmdnsengine else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/debug/ -lqmdnsengine
else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
@@ -121,14 +216,49 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
# Default rules for deployment. unix:!macx: {
qnx: target.path = /tmp/$${TARGET}/bin isEmpty(PREFIX) {
else: unix:!android: target.path = /opt/$${TARGET}/bin PREFIX = /usr/local
!isEmpty(target.path): INSTALLS += target }
isEmpty(BINDIR) {
BINDIR = bin
}
isEmpty(DATADIR) {
DATADIR = share
}
target.path = $$PREFIX/$$BINDIR/
desktop.files = deploy/linux/com.moonlight_stream.Moonlight.desktop
desktop.path = $$PREFIX/$$DATADIR/applications/
icons.files = res/moonlight.svg
icons.path = $$PREFIX/$$DATADIR/icons/hicolor/scalable/apps/
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appstream.path = $$PREFIX/$$DATADIR/metainfo/
appdata.files = gamecontrollerdb.txt
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
INSTALLS += target appdata desktop icons appstream
}
win32 {
RC_ICONS = moonlight.ico
QMAKE_TARGET_COMPANY = Moonlight Game Streaming Project
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
QMAKE_TARGET_PRODUCT = Moonlight
CONFIG -= embed_manifest_exe
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
}
macx { macx {
QMAKE_INFO_PLIST = $$PWD/Info.plist QMAKE_INFO_PLIST = $$PWD/Info.plist
APP_QML_FILES.files = res/macos.icns
APP_QML_FILES.path = Contents/Resources APP_BUNDLE_RESOURCES.files = moonlight.icns gamecontrollerdb.txt
QMAKE_BUNDLE_DATA += APP_QML_FILES APP_BUNDLE_RESOURCES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
} }
VERSION = 0.5.1
DEFINES += VERSION_STR=\\\"0.5.1\\\"
+142
View File
@@ -0,0 +1,142 @@
#include "autoupdatechecker.h"
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
QObject(parent),
m_Nam(this)
{
// Never communicate over HTTP
m_Nam.setStrictTransportSecurityEnabled(true);
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
QString currentVersion(VERSION_STR);
qDebug() << "Current Moonlight version:" << currentVersion;
parseStringToVersionQuad(currentVersion, m_CurrentVersionQuad);
// Should at least have a 1.0-style version number
Q_ASSERT(m_CurrentVersionQuad.count() > 1);
}
void AutoUpdateChecker::start()
{
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
// We'll get a callback when this is finished
QUrl url("https://moonlight-stream.com/updates/qt.json");
m_Nam.get(QNetworkRequest(url));
#endif
}
void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>& version)
{
QStringList list = string.split('.');
for (const QString& component : list) {
version.append(component.toInt());
}
}
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
{
Q_ASSERT(reply->isFinished());
if (reply->error() == QNetworkReply::NoError) {
QTextStream stream(reply);
stream.setCodec("UTF-8");
// Read all data and queue the reply for deletion
QString jsonString = stream.readAll();
reply->deleteLater();
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8(), &error);
if (jsonDoc.isNull()) {
qWarning() << "Update manifest malformed:" << error.errorString();
return;
}
QJsonArray array = jsonDoc.array();
if (array.isEmpty()) {
qWarning() << "Update manifest doesn't contain an array";
return;
}
for (QJsonValueRef updateEntry : array) {
if (updateEntry.isObject()) {
QJsonObject updateObj = updateEntry.toObject();
if (!updateObj.contains("platform") ||
!updateObj.contains("arch") ||
!updateObj.contains("version") ||
!updateObj.contains("browser_url")) {
qWarning() << "Update manifest entry missing vital field";
continue;
}
if (!updateObj["platform"].isString() ||
!updateObj["arch"].isString() ||
!updateObj["version"].isString() ||
!updateObj["browser_url"].isString()) {
qWarning() << "Update manifest entry has unexpected vital field type";
continue;
}
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
updateObj["platform"] == QSysInfo::productType()) {
qDebug() << "Found update manifest match for current platform";
QString latestVersion = updateObj["version"].toString();
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
QVector<int> latestVersionQuad;
parseStringToVersionQuad(latestVersion, latestVersionQuad);
for (int i = 0;; i++) {
int latestVer = 0;
int currentVer = 0;
// Treat missing decimal places as 0
if (i < latestVersionQuad.count()) {
latestVer = latestVersionQuad[i];
}
if (i < m_CurrentVersionQuad.count()) {
currentVer = m_CurrentVersionQuad[i];
}
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
break;
}
if (currentVer < latestVer) {
qDebug() << "Update available";
emit onUpdateAvailable(updateObj["browser_url"].toString());
return;
}
else if (currentVer > latestVer) {
qDebug() << "Update manifest version lower than current version";
return;
}
}
qDebug() << "Update manifest version equal to current version";
return;
}
}
else {
qWarning() << "Update manifest contained unrecognized entry:" << updateEntry.toString();
}
}
qWarning() << "No entry in update manifest found for current platform: "
<< QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
}
else {
qWarning() << "Update checking failed with error: " << reply->error();
reply->deleteLater();
}
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <QObject>
#include <QNetworkAccessManager>
class AutoUpdateChecker : public QObject
{
Q_OBJECT
public:
explicit AutoUpdateChecker(QObject *parent = nullptr);
Q_INVOKABLE void start();
signals:
void onUpdateAvailable(QString url);
private slots:
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
private:
void parseStringToVersionQuad(QString& string, QVector<int>& version);
QVector<int> m_CurrentVersionQuad;
QNetworkAccessManager m_Nam;
};
+9 -4
View File
@@ -1,14 +1,19 @@
#include "boxartmanager.h" #include "boxartmanager.h"
#include "../path.h"
#include <QStandardPaths>
#include <QImageReader> #include <QImageReader>
#include <QImageWriter> #include <QImageWriter>
BoxArtManager::BoxArtManager(QObject *parent) : BoxArtManager::BoxArtManager(QObject *parent) :
QObject(parent), QObject(parent),
m_BoxArtDir( m_BoxArtDir(Path::getBoxArtCacheDir()),
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart") m_ThreadPool(this)
{ {
// 4 is a good balance between fast loading for large
// app grids and not crushing GFE with tons of requests
// and causing UI jank from constantly stalling to decode
// new images.
m_ThreadPool.setMaxThreadCount(4);
if (!m_BoxArtDir.exists()) { if (!m_BoxArtDir.exists()) {
m_BoxArtDir.mkpath("."); m_BoxArtDir.mkpath(".");
} }
@@ -42,7 +47,7 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
// If we get here, we need to fetch asynchronously. // If we get here, we need to fetch asynchronously.
// Kick off a worker on our thread pool to do just that. // Kick off a worker on our thread pool to do just that.
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app); NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
QThreadPool::globalInstance()->start(netLoadTask); m_ThreadPool.start(netLoadTask);
// Return the placeholder then we can notify the caller // Return the placeholder then we can notify the caller
// later when the real image is ready. // later when the real image is ready.
+1
View File
@@ -36,6 +36,7 @@ private:
getFilePathForBoxArt(NvComputer* computer, int appId); getFilePathForBoxArt(NvComputer* computer, int appId);
QDir m_BoxArtDir; QDir m_BoxArtDir;
QThreadPool m_ThreadPool;
}; };
class NetworkBoxArtLoadTask : public QObject, public QRunnable class NetworkBoxArtLoadTask : public QObject, public QRunnable
+101 -19
View File
@@ -1,5 +1,6 @@
#include "computermanager.h" #include "computermanager.h"
#include "nvhttp.h" #include "nvhttp.h"
#include "settings/streamingpreferences.h"
#include <QThread> #include <QThread>
#include <QUdpSocket> #include <QUdpSocket>
@@ -51,6 +52,8 @@ NvComputer::NvComputer(QSettings& settings)
this->appVersion = nullptr; this->appVersion = nullptr;
this->maxLumaPixelsHEVC = 0; this->maxLumaPixelsHEVC = 0;
this->serverCodecModeSupport = 0; this->serverCodecModeSupport = 0;
this->pendingQuit = false;
this->gpuModel = nullptr;
} }
void void
@@ -90,7 +93,7 @@ void NvComputer::sortAppList()
NvComputer::NvComputer(QString address, QString serverInfo) NvComputer::NvComputer(QString address, QString serverInfo)
{ {
this->name = NvHTTP::getXmlString(serverInfo, "hostname"); this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isNull()) { if (this->name.isEmpty()) {
this->name = "UNKNOWN"; this->name = "UNKNOWN";
} }
@@ -104,7 +107,7 @@ NvComputer::NvComputer(QString address, QString serverInfo)
} }
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport"); QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
if (!codecSupport.isNull()) { if (!codecSupport.isEmpty()) {
this->serverCodecModeSupport = codecSupport.toInt(); this->serverCodecModeSupport = codecSupport.toInt();
} }
else { else {
@@ -112,7 +115,7 @@ NvComputer::NvComputer(QString address, QString serverInfo)
} }
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC"); QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
if (!maxLumaPixelsHEVC.isNull()) { if (!maxLumaPixelsHEVC.isEmpty()) {
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt(); this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
} }
else { else {
@@ -133,19 +136,21 @@ NvComputer::NvComputer(QString address, QString serverInfo)
this->currentGameId = NvHTTP::getCurrentGame(serverInfo); this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion"); this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion"); this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
this->activeAddress = address; this->activeAddress = address;
this->state = NvComputer::CS_ONLINE; this->state = NvComputer::CS_ONLINE;
this->pendingQuit = false;
} }
bool NvComputer::wake() bool NvComputer::wake()
{ {
if (state == NvComputer::CS_ONLINE) { if (state == NvComputer::CS_ONLINE) {
qWarning() << name << " is already online"; qWarning() << name << "is already online";
return true; return true;
} }
if (macAddress.isNull()) { if (macAddress.isEmpty()) {
qWarning() << name << " has no MAC address stored"; qWarning() << name << "has no MAC address stored";
return false; return false;
} }
@@ -173,6 +178,11 @@ bool NvComputer::wake()
for (QString& addressString : addressList) { for (QString& addressString : addressList) {
QHostInfo hostInfo = QHostInfo::fromName(addressString); QHostInfo hostInfo = QHostInfo::fromName(addressString);
if (hostInfo.error() != QHostInfo::NoError) {
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
continue;
}
// Try all IP addresses that this string resolves to // Try all IP addresses that this string resolves to
for (QHostAddress& address : hostInfo.addresses()) { for (QHostAddress& address : hostInfo.addresses()) {
QUdpSocket sock; QUdpSocket sock;
@@ -184,7 +194,7 @@ bool NvComputer::wake()
// Send to all ports // Send to all ports
for (quint16 port : WOL_PORTS) { for (quint16 port : WOL_PORTS) {
if (sock.writeDatagram(wolPayload, address, port)) { if (sock.writeDatagram(wolPayload, address, port)) {
qDebug() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port; qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
success = true; success = true;
} }
} }
@@ -207,7 +217,7 @@ QVector<QString> NvComputer::uniqueAddresses()
// Prune duplicates (always giving precedence to the first) // Prune duplicates (always giving precedence to the first)
for (int i = 0; i < uniqueAddressList.count(); i++) { for (int i = 0; i < uniqueAddressList.count(); i++) {
if (uniqueAddressList[i].isEmpty() || uniqueAddressList[i].isNull()) { if (uniqueAddressList[i].isEmpty()) {
uniqueAddressList.remove(i); uniqueAddressList.remove(i);
i--; i--;
continue; continue;
@@ -264,6 +274,7 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(gfeVersion); ASSIGN_IF_CHANGED(gfeVersion);
ASSIGN_IF_CHANGED(appVersion); ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC); ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList); ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes); ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
return changed; return changed;
@@ -286,6 +297,56 @@ ComputerManager::ComputerManager(QObject *parent)
settings.endArray(); settings.endArray();
} }
ComputerManager::~ComputerManager()
{
QWriteLocker lock(&m_Lock);
// Delete machines that haven't been resolved yet
while (!m_PendingResolution.isEmpty()) {
MdnsPendingComputer* computer = m_PendingResolution.first();
delete computer;
m_PendingResolution.removeFirst();
}
// Delete the browser to stop discovery
delete m_MdnsBrowser;
m_MdnsBrowser = nullptr;
{
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
// Interrupt all polling threads
while (i.hasNext()) {
i.next();
i.value()->requestInterruption();
}
// Wait for all threads to terminate before destroying
// the NvComputer objects.
i.toFront();
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
thread->wait();
delete thread;
i.remove();
}
}
{
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
// Destroy all NvComputer objects
while (i.hasNext()) {
i.next();
delete i.value();
i.remove();
}
}
}
void ComputerManager::saveHosts() void ComputerManager::saveHosts()
{ {
QSettings settings; QSettings settings;
@@ -308,17 +369,24 @@ void ComputerManager::startPolling()
return; return;
} }
// Start an MDNS query for GameStream hosts StreamingPreferences prefs;
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
this, [this](const QMdnsEngine::Service& service) {
qDebug() << "Discovered mDNS host: " << service.hostname();
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service); if (prefs.enableMdns) {
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)), // Start an MDNS query for GameStream hosts
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress))); m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
m_PendingResolution.append(pendingComputer); connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
}); this, [this](const QMdnsEngine::Service& service) {
qInfo() << "Discovered mDNS host:" << service.hostname();
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
m_PendingResolution.append(pendingComputer);
});
}
else {
qWarning() << "mDNS is disabled by user preference";
}
// Start polling threads for each known host // Start polling threads for each known host
QMapIterator<QString, NvComputer*> i(m_KnownHosts); QMapIterator<QString, NvComputer*> i(m_KnownHosts);
@@ -331,7 +399,7 @@ void ComputerManager::startPolling()
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer, void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
const QHostAddress& address) const QHostAddress& address)
{ {
qDebug() << "Resolved " << computer->hostname() << " to " << address.toString(); qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
addNewHost(address.toString(), true); addNewHost(address.toString(), true);
@@ -396,6 +464,15 @@ void ComputerManager::pairHost(NvComputer* computer, QString pin)
QThreadPool::globalInstance()->start(pairing); QThreadPool::globalInstance()->start(pairing);
} }
void ComputerManager::quitRunningApp(NvComputer* computer)
{
QWriteLocker lock(&computer->lock);
computer->pendingQuit = true;
PendingQuitTask* quit = new PendingQuitTask(this, computer);
QThreadPool::globalInstance()->start(quit);
}
void ComputerManager::stopPollingAsync() void ComputerManager::stopPollingAsync()
{ {
QWriteLocker lock(&m_Lock); QWriteLocker lock(&m_Lock);
@@ -447,6 +524,11 @@ ComputerManager::handleComputerStateChanged(NvComputer* computer)
{ {
emit computerStateChanged(computer); emit computerStateChanged(computer);
if (computer->pendingQuit && computer->currentGameId == 0) {
computer->pendingQuit = false;
emit quitAppCompleted(QVariant());
}
// Save updated hosts to QSettings // Save updated hosts to QSettings
saveHosts(); saveHosts();
} }
+77 -13
View File
@@ -16,10 +16,14 @@
class NvComputer class NvComputer
{ {
friend class PcMonitorThread; friend class PcMonitorThread;
friend class ComputerManager;
friend class PendingQuitTask;
private: private:
void sortAppList(); void sortAppList();
bool pendingQuit;
public: public:
explicit NvComputer(QString address, QString serverInfo); explicit NvComputer(QString address, QString serverInfo);
@@ -61,6 +65,7 @@ public:
QVector<NvDisplayMode> displayModes; QVector<NvDisplayMode> displayModes;
int maxLumaPixelsHEVC; int maxLumaPixelsHEVC;
int serverCodecModeSupport; int serverCodecModeSupport;
QString gpuModel;
// Persisted traits // Persisted traits
QString localAddress; QString localAddress;
@@ -98,7 +103,7 @@ private:
QString serverInfo; QString serverInfo;
try { try {
serverInfo = http.getServerInfo(); serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
} catch (...) { } catch (...) {
return false; return false;
} }
@@ -148,31 +153,31 @@ private:
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH; int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
while (!isInterruptionRequested()) { while (!isInterruptionRequested()) {
bool stateChanged = false; bool stateChanged = false;
for (int i = 0; i < TRIES_BEFORE_OFFLINING; i++) { bool online = false;
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) { for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) { if (isInterruptionRequested()) {
return; return;
} }
if (tryPollComputer(address, stateChanged)) { if (tryPollComputer(address, stateChanged)) {
if (!wasOnline) {
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
}
online = true;
break; break;
} }
} }
// No need to continue retrying if we're online
if (m_Computer->state == NvComputer::CS_ONLINE) {
break;
}
} }
// Check if we failed after all retry attempts // Check if we failed after all retry attempts
// Note: we don't need to acquire the read lock here, // Note: we don't need to acquire the read lock here,
// because we're on the writing thread. // because we're on the writing thread.
if (m_Computer->state != NvComputer::CS_ONLINE) { if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
if (m_Computer->state != NvComputer::CS_OFFLINE) { qInfo() << m_Computer->name << "is now offline";
m_Computer->state = NvComputer::CS_OFFLINE; m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true; stateChanged = true;
}
} }
// Grab the applist if it's empty or it's been long enough that we need to refresh // Grab the applist if it's empty or it's been long enough that we need to refresh
@@ -180,6 +185,13 @@ private:
if (m_Computer->state == NvComputer::CS_ONLINE && if (m_Computer->state == NvComputer::CS_ONLINE &&
m_Computer->pairState == NvComputer::PS_PAIRED && m_Computer->pairState == NvComputer::PS_PAIRED &&
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) { (m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
// Notify prior to the app list poll since it may take a while, and we don't
// want to delay onlining of a machine, especially if we already have a cached list.
if (stateChanged) {
emit computerStateChanged(m_Computer);
stateChanged = false;
}
if (updateAppList(stateChanged)) { if (updateAppList(stateChanged)) {
pollsSinceLastAppListFetch = 0; pollsSinceLastAppListFetch = 0;
} }
@@ -249,6 +261,8 @@ class ComputerManager : public QObject
public: public:
explicit ComputerManager(QObject *parent = nullptr); explicit ComputerManager(QObject *parent = nullptr);
virtual ~ComputerManager();
Q_INVOKABLE void startPolling(); Q_INVOKABLE void startPolling();
Q_INVOKABLE void stopPollingAsync(); Q_INVOKABLE void stopPollingAsync();
@@ -257,6 +271,8 @@ public:
void pairHost(NvComputer* computer, QString pin); void pairHost(NvComputer* computer, QString pin);
void quitRunningApp(NvComputer* computer);
QVector<NvComputer*> getComputers(); QVector<NvComputer*> getComputers();
// computer is deleted inside this call // computer is deleted inside this call
@@ -269,6 +285,8 @@ signals:
void computerAddCompleted(QVariant success); void computerAddCompleted(QVariant success);
void quitAppCompleted(QVariant error);
private slots: private slots:
void handleComputerStateChanged(NvComputer* computer); void handleComputerStateChanged(NvComputer* computer);
@@ -336,6 +354,49 @@ private:
QString m_Pin; QString m_Pin;
}; };
class PendingQuitTask : public QObject, public QRunnable
{
Q_OBJECT
public:
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
: m_Computer(computer)
{
connect(this, &PendingQuitTask::quitAppFailed,
computerManager, &ComputerManager::quitAppCompleted);
}
signals:
void quitAppFailed(QString error);
private:
void run()
{
NvHTTP http(m_Computer->activeAddress);
try {
if (m_Computer->currentGameId != 0) {
http.quitApp();
}
} catch (const GfeHttpResponseException& e) {
{
QWriteLocker lock(&m_Computer->lock);
m_Computer->pendingQuit = false;
}
if (e.getStatusCode() == 599) {
// 599 is a special code we make a custom message for
emit quitAppFailed("The running game wasn't started by this PC. "
"You must quit the game on the host PC manually or use the device that originally started the game.");
}
else {
emit quitAppFailed(e.toQString());
}
}
}
NvComputer* m_Computer;
};
class PendingAddTask : public QObject, public QRunnable class PendingAddTask : public QObject, public QRunnable
{ {
Q_OBJECT Q_OBJECT
@@ -362,9 +423,11 @@ private:
{ {
NvHTTP http(m_Address); NvHTTP http(m_Address);
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
QString serverInfo; QString serverInfo;
try { try {
serverInfo = http.getServerInfo(); serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
} catch (...) { } catch (...) {
if (!m_Mdns) { if (!m_Mdns) {
emit computerAddCompleted(false); emit computerAddCompleted(false);
@@ -400,6 +463,7 @@ private:
// Tell our client if something changed // Tell our client if something changed
if (changed) { if (changed) {
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
emit computerStateChanged(existingComputer); emit computerStateChanged(existingComputer);
} }
} }
+16 -11
View File
@@ -2,12 +2,12 @@
#include "utils.h" #include "utils.h"
#include <QDebug> #include <QDebug>
#include <QRandomGenerator64>
#include <openssl/pem.h> #include <openssl/pem.h>
#include <openssl/rsa.h> #include <openssl/rsa.h>
#include <openssl/bn.h> #include <openssl/bn.h>
#include <openssl/x509.h> #include <openssl/x509.h>
#include <openssl/rand.h>
#define SER_UNIQUEID "uniqueid" #define SER_UNIQUEID "uniqueid"
#define SER_CERT "certificate" #define SER_CERT "certificate"
@@ -101,7 +101,7 @@ void IdentityManager::createCredentials(QSettings& settings)
settings.setValue(SER_CERT, m_CachedPemCert); settings.setValue(SER_CERT, m_CachedPemCert);
settings.setValue(SER_KEY, m_CachedPrivateKey); settings.setValue(SER_KEY, m_CachedPrivateKey);
qDebug() << "Wrote new identity credentials to settings"; qInfo() << "Wrote new identity credentials to settings";
} }
IdentityManager::IdentityManager() IdentityManager::IdentityManager()
@@ -112,7 +112,7 @@ IdentityManager::IdentityManager()
m_CachedPrivateKey = settings.value(SER_KEY).toByteArray(); m_CachedPrivateKey = settings.value(SER_KEY).toByteArray();
if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) { if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) {
qDebug() << "No existing credentials found"; qInfo() << "No existing credentials found";
createCredentials(settings); createCredentials(settings);
} }
else if (getSslCertificate().isNull()) { else if (getSslCertificate().isNull()) {
@@ -157,10 +157,14 @@ IdentityManager::getSslKey()
// We must write out our PEM in the old PKCS1 format for SecureTransport // We must write out our PEM in the old PKCS1 format for SecureTransport
// on macOS/iOS to be able to read it. // on macOS/iOS to be able to read it.
#ifdef Q_OS_DARWIN
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
#else
PEM_write_bio_PrivateKey(bio, pk, nullptr, nullptr, 0, nullptr, 0);
#endif
BUF_MEM* mem; BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem); BIO_get_mem_ptr(bio, &mem);
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa); m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa);
BIO_free(bio); BIO_free(bio);
@@ -181,20 +185,21 @@ IdentityManager::getSslConfig()
QString QString
IdentityManager::getUniqueId() IdentityManager::getUniqueId()
{ {
if (m_CachedUniqueId.isNull()) { if (m_CachedUniqueId.isEmpty()) {
QSettings settings; QSettings settings;
// Load the unique ID from settings // Load the unique ID from settings
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString(); m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) { if (!m_CachedUniqueId.isEmpty()) {
qDebug() << "Loaded unique ID from settings: " << m_CachedUniqueId; qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
} }
else { else {
// Generate a new unique ID in base 16 // Generate a new unique ID in base 16
m_CachedUniqueId = QString::number( uint64_t uid;
QRandomGenerator64::securelySeeded().generate64(), 16); RAND_bytes(reinterpret_cast<unsigned char*>(&uid), sizeof(uid));
m_CachedUniqueId = QString::number(uid, 16);
qDebug() << "Generated new unique ID: " << m_CachedUniqueId; qInfo() << "Generated new unique ID:" << m_CachedUniqueId;
settings.setValue(SER_UNIQUEID, m_CachedUniqueId); settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
} }
+52 -17
View File
@@ -16,6 +16,8 @@
NvHTTP::NvHTTP(QString address) : NvHTTP::NvHTTP(QString address) :
m_Address(address) m_Address(address)
{ {
Q_ASSERT(!address.isEmpty());
m_BaseUrlHttp.setScheme("http"); m_BaseUrlHttp.setScheme("http");
m_BaseUrlHttps.setScheme("https"); m_BaseUrlHttps.setScheme("https");
m_BaseUrlHttp.setHost(address); m_BaseUrlHttp.setHost(address);
@@ -27,9 +29,15 @@ NvHTTP::NvHTTP(QString address) :
QVector<int> QVector<int>
NvHTTP::parseQuad(QString quad) NvHTTP::parseQuad(QString quad)
{ {
QStringList parts = quad.split(".");
QVector<int> ret; QVector<int> ret;
// Return an empty vector for old GFE versions
// that were missing GfeVersion.
if (quad.isEmpty()) {
return ret;
}
QStringList parts = quad.split(".");
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
ret.append(parts.at(i).toInt()); ret.append(parts.at(i).toInt());
@@ -56,7 +64,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
} }
QString QString
NvHTTP::getServerInfo() NvHTTP::getServerInfo(NvLogLevel logLevel)
{ {
QString serverInfo; QString serverInfo;
@@ -67,7 +75,8 @@ NvHTTP::getServerInfo()
serverInfo = openConnectionToString(m_BaseUrlHttps, serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo", "serverinfo",
nullptr, nullptr,
true); true,
logLevel);
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(serverInfo); verifyResponseStatus(serverInfo);
} }
@@ -79,7 +88,8 @@ NvHTTP::getServerInfo()
serverInfo = openConnectionToString(m_BaseUrlHttp, serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo", "serverinfo",
nullptr, nullptr,
true); true,
logLevel);
verifyResponseStatus(serverInfo); verifyResponseStatus(serverInfo);
} }
else else
@@ -123,6 +133,11 @@ NvHTTP::launchApp(int appId,
bool localAudio, bool localAudio,
int gamepadMask) int gamepadMask)
{ {
int riKeyId;
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
riKeyId = qFromBigEndian(riKeyId);
QString response = QString response =
openConnectionToString(m_BaseUrlHttps, openConnectionToString(m_BaseUrlHttps,
"launch", "launch",
@@ -132,7 +147,7 @@ NvHTTP::launchApp(int appId,
QString::number(streamConfig->fps)+ QString::number(streamConfig->fps)+
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+ "&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+ "&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+ "&rikeyid="+QString::number(riKeyId)+
(streamConfig->enableHdr ? (streamConfig->enableHdr ?
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" : "&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
"")+ "")+
@@ -149,11 +164,16 @@ NvHTTP::launchApp(int appId,
void void
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig) NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
{ {
int riKeyId;
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
riKeyId = qFromBigEndian(riKeyId);
QString response = QString response =
openConnectionToString(m_BaseUrlHttps, openConnectionToString(m_BaseUrlHttps,
"resume", "resume",
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+ "rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+ "&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration), "&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
false); false);
@@ -175,7 +195,7 @@ NvHTTP::quitApp()
// Newer GFE versions will just return success even if quitting fails // Newer GFE versions will just return success even if quitting fails
// if we're not the original requestor. // if we're not the original requestor.
if (getCurrentGame(getServerInfo()) != 0) { if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
// Generate a synthetic GfeResponseException letting the caller know // Generate a synthetic GfeResponseException letting the caller know
// that they can't kill someone else's stream. // that they can't kill someone else's stream.
throw GfeHttpResponseException(599, ""); throw GfeHttpResponseException(599, "");
@@ -215,7 +235,8 @@ NvHTTP::getAppList()
QString appxml = openConnectionToString(m_BaseUrlHttps, QString appxml = openConnectionToString(m_BaseUrlHttps,
"applist", "applist",
nullptr, nullptr,
true); true,
NvLogLevel::ERROR);
verifyResponseStatus(appxml); verifyResponseStatus(appxml);
QXmlStreamReader xmlReader(appxml); QXmlStreamReader xmlReader(appxml);
@@ -265,7 +286,10 @@ NvHTTP::verifyResponseStatus(QString xml)
else else
{ {
QString statusMessage = xmlReader.attributes().value("status_message").toString(); QString statusMessage = xmlReader.attributes().value("status_message").toString();
qDebug() << "Request failed: " << statusCode << " " << statusMessage; if (statusCode != 401) {
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
qWarning() << "Request failed:" << statusCode << statusMessage;
}
throw GfeHttpResponseException(statusCode, statusMessage); throw GfeHttpResponseException(statusCode, statusMessage);
} }
} }
@@ -279,7 +303,8 @@ NvHTTP::getBoxArt(int appId)
"appasset", "appasset",
"appid="+QString::number(appId)+ "appid="+QString::number(appId)+
"&AssetType=2&AssetIdx=0", "&AssetType=2&AssetIdx=0",
true); true,
NvLogLevel::VERBOSE);
QImage image = QImageReader(reply).read(); QImage image = QImageReader(reply).read();
delete reply; delete reply;
@@ -325,12 +350,15 @@ QString
NvHTTP::openConnectionToString(QUrl baseUrl, NvHTTP::openConnectionToString(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout) bool enableTimeout,
NvLogLevel logLevel)
{ {
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout); QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
QString ret; QString ret;
ret = QTextStream(reply).readAll(); QTextStream stream(reply);
stream.setCodec("UTF-8");
ret = stream.readAll();
delete reply; delete reply;
return ret; return ret;
@@ -340,7 +368,8 @@ QNetworkReply*
NvHTTP::openConnection(QUrl baseUrl, NvHTTP::openConnection(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout) bool enableTimeout,
NvLogLevel logLevel)
{ {
// Build a URL for the request // Build a URL for the request
QUrl url(baseUrl); QUrl url(baseUrl);
@@ -366,13 +395,17 @@ NvHTTP::openConnection(QUrl baseUrl,
{ {
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit())); QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
} }
qDebug() << "Executing request: " << url.toString(); if (logLevel >= NvLogLevel::VERBOSE) {
qInfo() << "Executing request:" << url.toString();
}
loop.exec(QEventLoop::ExcludeUserInputEvents); loop.exec(QEventLoop::ExcludeUserInputEvents);
// Abort the request if it timed out // Abort the request if it timed out
if (!reply->isFinished()) if (!reply->isFinished())
{ {
qDebug() << "Aborting timed out request for " << url.toString(); if (logLevel >= NvLogLevel::ERROR) {
qWarning() << "Aborting timed out request for" << url.toString();
}
reply->abort(); reply->abort();
} }
@@ -383,7 +416,9 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error // Handle error
if (reply->error() != QNetworkReply::NoError) if (reply->error() != QNetworkReply::NoError)
{ {
qDebug() << command << " request failed with error " << reply->error(); if (logLevel >= NvLogLevel::ERROR) {
qWarning() << command << " request failed with error " << reply->error();
}
GfeHttpResponseException exception(reply->error(), reply->errorString()); GfeHttpResponseException exception(reply->error(), reply->errorString());
delete reply; delete reply;
throw exception; throw exception;
+12 -4
View File
@@ -17,7 +17,7 @@ public:
bool isInitialized() bool isInitialized()
{ {
return id != 0 && !name.isNull(); return id != 0 && !name.isEmpty();
} }
int id; int id;
@@ -80,6 +80,12 @@ private:
class NvHTTP class NvHTTP
{ {
public: public:
enum NvLogLevel {
NONE,
ERROR,
VERBOSE
};
explicit NvHTTP(QString address); explicit NvHTTP(QString address);
static static
@@ -87,7 +93,7 @@ public:
getCurrentGame(QString serverInfo); getCurrentGame(QString serverInfo);
QString QString
getServerInfo(); getServerInfo(NvLogLevel logLevel);
static static
void void
@@ -107,7 +113,8 @@ public:
openConnectionToString(QUrl baseUrl, openConnectionToString(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout); bool enableTimeout,
NvLogLevel logLevel = NvLogLevel::VERBOSE);
static static
QVector<int> QVector<int>
@@ -143,7 +150,8 @@ private:
openConnection(QUrl baseUrl, openConnection(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout); bool enableTimeout,
NvLogLevel logLevel);
QString m_Address; QString m_Address;
QNetworkAccessManager m_Nam; QNetworkAccessManager m_Nam;
+26 -24
View File
@@ -1,8 +1,6 @@
#include "nvpairingmanager.h" #include "nvpairingmanager.h"
#include "utils.h" #include "utils.h"
#include <QRandomGenerator>
#include <openssl/bio.h> #include <openssl/bio.h>
#include <openssl/aes.h> #include <openssl/aes.h>
#include <openssl/rand.h> #include <openssl/rand.h>
@@ -21,7 +19,7 @@ NvPairingManager::NvPairingManager(QString address) :
BIO_free_all(bio); BIO_free_all(bio);
if (m_Cert == nullptr) if (m_Cert == nullptr)
{ {
throw new std::runtime_error("Unable to load certificate"); throw std::runtime_error("Unable to load certificate");
} }
QByteArray pk = IdentityManager::get()->getPrivateKey(); QByteArray pk = IdentityManager::get()->getPrivateKey();
@@ -32,7 +30,7 @@ NvPairingManager::NvPairingManager(QString address) :
BIO_free_all(bio); BIO_free_all(bio);
if (m_PrivateKey == nullptr) if (m_PrivateKey == nullptr)
{ {
throw new std::runtime_error("Unable to load private key"); throw std::runtime_error("Unable to load private key");
} }
} }
@@ -89,13 +87,15 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_free_all(bio); BIO_free_all(bio);
#if (OPENSSL_VERSION_NUMBER < 0x10100000L) #if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature; ASN1_BIT_STRING *asnSignature = cert->signature;
#else #elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
const ASN1_BIT_STRING *asnSignature; ASN1_BIT_STRING *asnSignature;
#endif
X509_get0_signature(&asnSignature, NULL, cert); X509_get0_signature(&asnSignature, NULL, cert);
#else
const ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, cert);
#endif
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length); QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
@@ -164,7 +164,7 @@ NvPairingManager::PairState
NvPairingManager::pair(QString appVersion, QString pin) NvPairingManager::pair(QString appVersion, QString pin)
{ {
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0); int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
qDebug() << "Pairing with server generation: " << serverMajorVersion; qInfo() << "Pairing with server generation:" << serverMajorVersion;
QCryptographicHash::Algorithm hashAlgo; QCryptographicHash::Algorithm hashAlgo;
int hashLength; int hashLength;
@@ -196,14 +196,14 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(getCert); NvHTTP::verifyResponseStatus(getCert);
if (NvHTTP::getXmlString(getCert, "paired") != "1") if (NvHTTP::getXmlString(getCert, "paired") != "1")
{ {
qDebug() << "Failed pairing at stage #1"; qCritical() << "Failed pairing at stage #1";
return PairState::FAILED; return PairState::FAILED;
} }
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert"); QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
if (serverCert == nullptr) if (serverCert == nullptr)
{ {
qDebug() << "Server likely already pairing"; qCritical() << "Server likely already pairing";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::ALREADY_IN_PROGRESS; return PairState::ALREADY_IN_PROGRESS;
} }
@@ -218,7 +218,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(challengeXml); NvHTTP::verifyResponseStatus(challengeXml);
if (NvHTTP::getXmlString(challengeXml, "paired") != "1") if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
{ {
qDebug() << "Failed pairing at stage #2"; qCritical() << "Failed pairing at stage #2";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED; return PairState::FAILED;
} }
@@ -228,13 +228,15 @@ NvPairingManager::pair(QString appVersion, QString pin)
QByteArray challengeResponse; QByteArray challengeResponse;
QByteArray serverResponse(challengeResponseData.data(), hashLength); QByteArray serverResponse(challengeResponseData.data(), hashLength);
#if (OPENSSL_VERSION_NUMBER < 0x10100000L) #if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature = m_Cert->signature;
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature; ASN1_BIT_STRING *asnSignature;
#else
const ASN1_BIT_STRING *asnSignature;
#endif
X509_get0_signature(&asnSignature, NULL, m_Cert); X509_get0_signature(&asnSignature, NULL, m_Cert);
#else
const ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, m_Cert);
#endif
challengeResponse.append(challengeResponseData.data() + hashLength, 16); challengeResponse.append(challengeResponseData.data() + hashLength, 16);
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length); challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
@@ -249,7 +251,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(respXml); NvHTTP::verifyResponseStatus(respXml);
if (NvHTTP::getXmlString(respXml, "paired") != "1") if (NvHTTP::getXmlString(respXml, "paired") != "1")
{ {
qDebug() << "Failed pairing at stage #3"; qCritical() << "Failed pairing at stage #3";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED; return PairState::FAILED;
} }
@@ -262,7 +264,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
serverSignature, serverSignature,
serverCert)) serverCert))
{ {
qDebug() << "MITM detected"; qCritical() << "MITM detected";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED; return PairState::FAILED;
} }
@@ -273,7 +275,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
expectedResponseData.append(serverSecret); expectedResponseData.append(serverSecret);
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse) if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
{ {
qDebug() << "Incorrect PIN"; qCritical() << "Incorrect PIN";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::PIN_WRONG; return PairState::PIN_WRONG;
} }
@@ -290,7 +292,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(secretRespXml); NvHTTP::verifyResponseStatus(secretRespXml);
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1") if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
{ {
qDebug() << "Failed pairing at stage #4"; qCritical() << "Failed pairing at stage #4";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED; return PairState::FAILED;
} }
@@ -302,7 +304,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(pairChallengeXml); NvHTTP::verifyResponseStatus(pairChallengeXml);
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1") if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
{ {
qDebug() << "Failed pairing at stage #5"; qCritical() << "Failed pairing at stage #5";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED; return PairState::FAILED;
} }
@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>com.moonlight_stream.Moonlight</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Moonlight</name>
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
<description>
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
<p>Features include:</p>
<ul>
<li>Streaming at up to 4K resolution</li>
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
<li>5.1 surround sound audio support</li>
<li>HEVC support for better video compression efficiency</li>
<li>Gamepad support with SDL mapping compatibility</li>
</ul>
</description>
<url type="homepage">https://moonlight-stream.com</url>
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
<screenshots>
<screenshot type="default">
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
</screenshot>
<screenshot>
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.5.0" date="2018-09-15">
<description>
<p>New features:</p>
<ul>
<li>Added option to enable mouse acceleration for remote desktop usage</li>
<li>Added option to disable automatic PC discovery</li>
<li>Added gamepad mappings from gabomdq's SDL_GameControllerDB</li>
<li>Added help links to error dialogs</li>
<li>Added quit shortcut tip to stream loading page</li>
<li>Restored the autodetect audio configuration option</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed quit app dialog starting the wrong app</li>
<li>Fixed very high input lag on GFE 3.14.0 and earlier</li>
<li>Fixed sending errant mouse click when capturing the mouse</li>
</ul>
</description>
</release>
<release version="0.4.0" date="2018-09-08">
<description>
<p>New features:</p>
<ul>
<li>Added option to enable (unsupported) 90/120 FPS streaming on 60 Hz monitors</li>
<li>Added refresh rate matching for better frame pacing on high refresh rate monitors</li>
<li>The streaming window now appears on the same monitor the UI was displayed on</li>
<li>The streaming window is now centered on the target monitor</li>
<li>Display a dialog when the connection is terminated by the remote PC</li>
<li>Added option to select between full-screen exclusive and borderless windowed mode</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed streaming with no audio device</li>
<li>Fixed mouse lag with some high polling rate mice</li>
<li>Improved UI performance on the app grid page</li>
<li>1440p and 4K 120 FPS performance is improved to avoid encoder bottlenecks</li>
<li>Autodetect audio configuration option removed because it was broken</li>
</ul>
</description>
</release>
<release version="0.3.0" date="2018-08-21">
<description>
<p>Bugfixes:</p>
<ul>
<li>Reduced power consumption when the app is minimized</li>
<li>Fixed malformed asset requests being sent when the first host in the grid is offline</li>
</ul>
</description>
</release>
<release version="0.2.0" date="2018-08-12">
<description>
<p>Bugfixes:</p>
<ul>
<li>Fixed functionality of '=', 'Numpad Dot', and 'Numpad Enter' keys</li>
<li>Fixed uninitialized variable causing strange gamepad behavior</li>
<li>Added a warning for incompatible configs and decoder failures</li>
<li>Added a warning for Wayland performance issues</li>
<li>Stopped capturing the mouse by default for windowed streams</li>
</ul>
</description>
</release>
<release version="0.1.0" date="2018-08-05">
<description>
<p>New features:</p>
<ul>
<li>Allow streaming at the client's native resolution</li>
<li>VDPAU hardware decoding support for NVIDIA GPUs</li>
<li>Updated UI to replace status text with icons</li>
<li>Stream window is now resizable</li>
<li>Display warnings during game launch for configuration issues</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Gamepad input now works even if the app doesn't have focus</li>
<li>Fixed streaming black screen if the host supported HEVC but the client didn't</li>
<li>Fixed broken video scaling causing extra stretching and off-center video</li>
<li>Fixed gamepad input issue causing some Y axis inputs to be reversed</li>
</ul>
</description>
</release>
<release version="0.0.5" date="2018-08-01">
<description>
<p>Changes from 0.0.4:</p>
<ul>
<li>Fixed a bug causing increased frame drops and input lag</li>
<li>Added support for quitting running games</li>
<li>Added support for waking sleeping PCs with Wake-on-LAN</li>
<li>Increased the FPS limit to 120 FPS</li>
</ul>
</description>
</release>
<release version="0.0.4a" date="2018-07-30">
<description>
<p>Initial pre-alpha release for Linux. Please file bugs!</p>
</description>
</release>
</releases>
<categories>
<category>Game</category>
<category>Qt</category>
</categories>
<developer_name>Moonlight Game Streaming Team</developer_name>
<update_contact>cameron@moonlight-stream.com</update_contact>
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
<provides>
<binary>moonlight</binary>
</provides>
<content_rating type="oars-1.0" />
</component>
@@ -0,0 +1,9 @@
[Desktop Entry]
Name=Moonlight
Comment=Stream games from your NVIDIA GameStream-enabled PC
Exec=moonlight
Icon=moonlight
Terminal=false
Type=Application
Categories=Qt;Game;
Keywords=nvidia;gamestream;stream;
+438
View File
@@ -0,0 +1,438 @@
# Game Controller DB for SDL in 2.0.6 format
# Source: https://github.com/gabomdq/SDL_GameControllerDB
# Windows
03000000fa2d00000100000000000000,3DRUDDER,leftx:a0,lefty:a1,rightx:a5,righty:a2,platform:Windows,
03000000022000000090000000000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
03000000203800000900000000000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
03000000c82d00000060000000000000,8Bitdo SF30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
03000000c82d00000061000000000000,8Bitdo SF30 Pro Wireless,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
03000000102800000900000000000000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows,
03000000a00500003232000000000000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Windows,
030000008f0e00001200000000000000,Acme,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Windows,
03000000341a00003608000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000c01100001352000000000000,Battalife Joystick,a:b6,b:b7,back:b2,leftshoulder:b0,leftx:a0,lefty:a1,rightshoulder:b1,start:b3,x:b4,y:b5,platform:Windows,
030000006b1400000055000000000000,bigben ps3padstreetnew,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
0300000066f700000500000000000000,BrutalLegendTest,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
03000000d81d00000b00000000000000,BUFFALO BSGP1601 Series ,a:b5,b:b3,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b9,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b13,x:b4,y:b2,platform:Windows,
03000000e82000006058000000000000,Cideko AK08b,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
030000005e0400008e02000000000000,Controller (XBOX 360 For Windows),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
03000000260900008888000000000000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a4,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Windows,
03000000a306000022f6000000000000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000791d00000103000000000000,Dual Box WII,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
030000004f04000023b3000000000000,Dual Trigger 3-in-1,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000341a00000108000000000000,EXEQ RF USB Gamepad 8206,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
030000000d0f00008500000000000000,Fighting Commander 2016 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008400000000000000,Fighting Commander 5,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008700000000000000,Fighting Stick mini 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008800000000000000,Fighting Stick mini 4,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,platform:Windows,
030000000d0f00002700000000000000,FIGHTING STICK V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
78696e70757403000000000000000000,Fightstick TES,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b2,y:b3,platform:Windows,
03000000790000000600000000000000,G-Shark GS-GP702,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b3,y:b0,platform:Windows,
03000000260900002625000000000000,Gamecube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,lefttrigger:a4,leftx:a0,lefty:a1,righttrigger:a5,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Windows,
030000008f0e00000d31000000000000,GAMEPAD 3 TURBO,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000280400000140000000000000,GamePad Pro USB,a:b1,b:b2,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
03000000ffff00000000000000000000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000451300000010000000000000,Generic USB Joystick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000341a00000302000000000000,Hama Scorpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00004900000000000000,Hatsune Miku Sho Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000d81400000862000000000000,HitBox Edition Cthulhu+,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b4,rightshoulder:b7,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00005f00000000000000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00005e00000000000000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00004000000000000000,Hori Fighting Stick Mini 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b4,rightshoulder:b7,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00006e00000000000000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00006600000000000000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f0000ee00000000000000,HORIPAD mini4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00004d00000000000000,HORIPAD3 A,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000250900000017000000000000,HRAP2 on PS/SS/N64 Joypad to USB BOX,a:b2,b:b1,back:b9,leftshoulder:b5,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b6,start:b8,x:b3,y:b0,platform:Windows,
030000008f0e00001330000000000000,HuiJia SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b9,x:b3,y:b0,platform:Windows,
03000000d81d00000f00000000000000,iBUFFALO BSGP1204 Series,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000d81d00001000000000000000,iBUFFALO BSGP1204P Series,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000830500006020000000000000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Windows,
03000000b50700001403000000000000,IMPACT BLACK,a:b2,b:b3,back:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
030000006f0e00002401000000000000,INJUSTICE FightStick for PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
03000000491900000204000000000000,Ipega PG-9023,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows,
030000006d04000011c2000000000000,Logitech Cordless Wingman,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b5,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b2,righttrigger:b7,rightx:a3,righty:a4,x:b4,platform:Windows,
030000006d04000016c2000000000000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000006d04000018c2000000000000,Logitech F510 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000006d04000019c2000000000000,Logitech F710 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700005032000000000000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700005082000000000000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008433000000000000,Mad Catz FightStick TE S+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008483000000000000,Mad Catz FightStick TE S+ PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b6,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008134000000000000,Mad Catz FightStick TE2+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b7,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b4,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008184000000000000,Mad Catz FightStick TE2+ PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,leftstick:b10,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008034000000000000,Mad Catz TE2 PS3 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008084000000000000,Mad Catz TE2 PS4 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008532000000000000,Madcatz Arcade Fightstick TE S PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700003888000000000000,Madcatz Arcade Fightstick TE S+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700001888000000000000,MadCatz SFIV FightStick PS3,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b4,righttrigger:b6,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000380700008081000000000000,MADCATZ SFV Arcade FightStick Alpha PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000008305000031b0000000000000,MaxfireBlaze3,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000250900000128000000000000,Mayflash Arcade Stick,a:b1,b:b2,back:b8,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b5,y:b6,platform:Windows,
03000000790000004418000000000000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
03000000790000004318000000000000,Mayflash GameCube Controller Adapter,a:b1,b:b2,back:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b0,leftshoulder:b4,leftstick:b0,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b0,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
030000008f0e00001030000000000000,Mayflash USB Adapter for original Sega Saturn controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b5,rightshoulder:b2,righttrigger:b7,start:b9,x:b3,y:b4,platform:Windows,
0300000025090000e803000000000000,Mayflash Wii Classic Controller,a:b1,b:b0,back:b8,dpdown:b13,dpleft:b12,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Windows,
03000000790000000018000000000000,Mayflash WiiU Pro Game Controller Adapter (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000250900006688000000000000,MP-8866 Super Dual Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows,
030000001008000001e5000000000000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,platform:Windows,
03000000bd12000015d0000000000000,Nintendo Retrolink USB Super SNES Classic Controller,a:b2,b:b1,back:b8,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows,
030000007e0500000920000000000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
030000004b120000014d000000000000,NYKO AIRFLO,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:a3,leftstick:a0,lefttrigger:b6,rightshoulder:b5,rightstick:a2,righttrigger:b7,start:b9,x:b2,y:b3,platform:Windows,
03000000362800000100000000000000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:b13,rightx:a3,righty:a4,x:b1,y:b2,platform:Windows,
03000000120c0000f60e000000000000,P4 Wired Gamepad,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b5,lefttrigger:b7,rightshoulder:b4,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows,
030000008f0e00000300000000000000,Piranha xtreme,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
03000000d62000006dca000000000000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000008f0e00007530000000000000,PS (R) Gamepad,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b1,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000e30500009605000000000000,PS to USB convert cable,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows,
03000000100800000100000000000000,PS1 USB,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
03000000100800000300000000000000,PS2 USB,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a4,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
030000004c0500006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows,
03000000888800000803000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b0,y:b3,platform:Windows,
03000000250900000500000000000000,PS3 DualShock,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows,
03000000100000008200000000000000,PS360+ v1.66,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:h0.4,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
030000004c050000a00b000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000004c050000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000004c050000cc09000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000300f00000011000000000000,QanBa Arcade JoyStick 1008,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b10,x:b0,y:b3,platform:Windows,
03000000300f00001611000000000000,QanBa Arcade JoyStick 4018,a:b1,b:b2,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,platform:Windows,
03000000222c00000020000000000000,QANBA DRONE ARCADE JOYSTICK,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,rightshoulder:b5,righttrigger:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000300f00001210000000000000,QanBa Joystick Plus,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b2,y:b3,platform:Windows,
03000000341a00000104000000000000,QanBa Joystick Q4RAF,a:b5,b:b6,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b1,y:b2,platform:Windows,
03000000222c00000223000000000000,Qanba Obsidian Arcade Joystick PS3 Mode,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000222c00000023000000000000,Qanba Obsidian Arcade Joystick PS4 Mode,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000321500000003000000000000,Razer Hydra,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
030000000d0f00001100000000000000,REAL ARCADE PRO.3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00006a00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00006b00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008a00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008b00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00007000000000000000,REAL ARCADE PRO.4 VLX,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00002200000000000000,REAL ARCADE Pro.V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00005b00000000000000,Real Arcade Pro.V4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00005c00000000000000,Real Arcade Pro.V4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000790000001100000000000000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows,
0300000000f000000300000000000000,RetroUSB.com RetroPad,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Windows,
0300000000f00000f100000000000000,RetroUSB.com Super RetroPort,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Windows,
030000006b140000010d000000000000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000006f0e00001e01000000000000,Rock Candy Gamepad for PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000004f04000003d0000000000000,run'n'drive,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b7,leftshoulder:a3,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:a4,rightstick:b11,righttrigger:b5,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000a30600001af5000000000000,Saitek Cyborg,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000a306000023f6000000000000,Saitek Cyborg V.1 Game pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000300f00001201000000000000,Saitek Dual Analog Pad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
03000000a30600000cff000000000000,Saitek P2500 Force Rumble Pad,a:b2,b:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,x:b0,y:b1,platform:Windows,
03000000a30600000c04000000000000,Saitek P2900,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
03000000300f00001001000000000000,Saitek P480 Rumble Pad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
03000000a30600000b04000000000000,Saitek P990,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
03000000a30600000b04000000010000,Saitek P990 Dual Analog Pad,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b8,x:b0,y:b3,platform:Windows,
03000000300f00001101000000000000,saitek rumble pad,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
0300000000050000289b000000000000,Saturn_Adapter_2.0,a:b1,b:b2,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows,
030000009b2800000500000000000000,Saturn_Adapter_2.0,a:b1,b:b2,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows,
03000000341a00000208000000000000,SL-6555-SBK,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:-a4,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a4,rightx:a3,righty:a2,start:b7,x:b2,y:b3,platform:Windows,
030000008f0e00000800000000000000,SpeedLink Strike FX Wireless,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000ff1100003133000000000000,SVEN X-PAD,a:b2,b:b3,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a4,start:b5,x:b0,y:b1,platform:Windows,
03000000fa1900000706000000000000,Team 5,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000b50700001203000000000000,Techmobility X6-38V,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
030000004f04000015b3000000000000,Thrustmaster Dual Analog 2,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows,
030000004f04000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Windows,
030000004f04000004b3000000000000,Thrustmaster Firestorm Dual Power 3,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows,
03000000666600000488000000000000,TigerGame PS/PS2 Game Controller Adapter,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows,
03000000d90400000200000000000000,TwinShock PS2,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
03000000380700006652000000000000,UnKnown,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000632500002305000000000000,USB Vibration Joystick (BM),a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000790000001b18000000000000,Venom Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
03000000450c00002043000000000000,XEOX Gamepad SL-6556-BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000172700004431000000000000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a7,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows,
03000000786901006e70000000000000,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
# Mac OS X
03000000022000000090000001000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X,
03000000203800000900000000010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X,
03000000102800000900000000000000,8Bitdo SFC30 GamePad Joystick,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X,
03000000a00500003232000008010000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X,
03000000a00500003232000009010000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X,
030000008305000031b0000000000000,Cideko AK08b,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000260900008888000088020000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a5,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Mac OS X,
03000000a306000022f6000001030000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000790000000600000000000000,G-Shark GP-702,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000ad1b000001f9000000000000,Gamestop BB-070 X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X,
030000000d0f00005f00000000010000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00005e00000000010000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00005f00000000000000,HORI Fighting Commander 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00005e00000000000000,HORI Fighting Commander 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00004d00000000000000,HORI Gem Pad 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00006e00000000010000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00006600000000010000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00006600000000000000,HORIPAD FPS PLUS 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000008f0e00001330000011010000,HuiJia SNES Controller,a:b4,b:b2,back:b16,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,leftshoulder:b12,rightshoulder:b14,start:b18,x:b6,y:b0,platform:Mac OS X,
03000000830500006020000000010000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Mac OS X,
03000000830500006020000000000000,iBuffalo USB 2-axis 8-button Gamepad,a:b1,b:b0,back:b6,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Mac OS X,
030000007e0500000620000001000000,Joy-Con (L),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,leftshoulder:b4,leftstick:b10,rightshoulder:b5,start:b8,x:b2,y:b3,platform:Mac OS X,
030000006d04000016c2000000020000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d04000016c2000000030000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d04000016c2000014040000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d04000016c2000000000000,Logitech F310 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d04000018c2000000000000,Logitech F510 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d0400001fc2000000000000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000006d04000019c2000000000000,Logitech Wireless Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000380700005032000000010000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000380700005082000000010000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000790000004418000000010000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Mac OS X,
0300000025090000e803000000000000,Mayflash Wii Classic Controller,a:b1,b:b0,back:b8,dpdown:b13,dpleft:b12,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Mac OS X,
03000000790000000018000000000000,Mayflash WiiU Pro Game Controller Adapter (DInput),a:b4,b:b8,back:b32,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b16,leftstick:b40,lefttrigger:b24,leftx:a0,lefty:a4,rightshoulder:b20,rightstick:b44,righttrigger:b28,rightx:a8,righty:a12,start:b36,x:b0,y:b12,platform:Mac OS X,
03000000d8140000cecf000000000000,MC Cthulhu,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X,
030000001008000001e5000006010000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,platform:Mac OS X,
030000007e0500000920000000000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X,
030000008f0e00000300000000000000,Piranha xtreme,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000d62000006dca000000010000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c0500006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X,
030000004c0500006802000000010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X,
030000004c050000a00b000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c050000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c050000c405000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c050000cc09000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000008916000000fd000000000000,Razer Onza TE,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000321500000010000000010000,Razer RAIJU,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
0300000032150000030a000000000000,Razer Wildcat,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000790000001100000000000000,Retrolink Classic Controller,a:b2,b:b1,back:b8,leftshoulder:b4,leftx:a3,lefty:a4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000790000001100000006010000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
030000006b140000010d000000010000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000c6240000fefa000000000000,Rock Candy Gamepad for PS3,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000811700007e05000000000000,Sega Saturn,a:b2,b:b4,dpdown:b16,dpleft:b15,dpright:b14,dpup:b17,leftshoulder:b8,lefttrigger:a5,leftx:a0,lefty:a2,rightshoulder:b9,righttrigger:a4,start:b13,x:b0,y:b6,platform:Mac OS X,
03000000b40400000a01000000000000,Sega Saturn USB Gamepad,a:b0,b:b1,back:b5,guide:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Mac OS X,
030000003512000021ab000000000000,SFC30 Joystick,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X,
030000004c050000cc09000000000000,Sony DualShock 4 V2,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c050000a00b000000000000,Sony DualShock 4 Wireless Adaptor,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000005e0400008e02000001000000,Steam Virtual GamePad,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000110100002014000000000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b12,x:b2,y:b3,platform:Mac OS X,
03000000110100002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X,
03000000381000002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X,
03000000110100001714000000000000,SteelSeries Stratus XL,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,start:b12,x:b2,y:b3,platform:Mac OS X,
03000000110100001714000020010000,SteelSeries Stratus XL,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,start:b12,x:b2,y:b3,platform:Mac OS X,
030000004f04000015b3000000000000,Thrustmaster Dual Analog 3.2,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Mac OS X,
030000004f04000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Mac OS X,
03000000bd12000015d0000000000000,Tomee SNES USB Controller,a:b2,b:b1,back:b8,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000bd12000015d0000000010000,Tomee SNES USB Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000100800000100000000000000,Twin USB Joystick,a:b4,b:b2,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b12,leftstick:b20,lefttrigger:b8,leftx:a0,lefty:a2,rightshoulder:b14,rightstick:b22,righttrigger:b10,rightx:a6,righty:a4,start:b18,x:b6,y:b0,platform:Mac OS X,
050000005769696d6f74652028303000,Wii Remote,a:b4,b:b5,back:b7,dpdown:b3,dpleft:b0,dpright:b1,dpup:b2,guide:b8,leftshoulder:b11,lefttrigger:b12,leftx:a0,lefty:a1,start:b6,x:b10,y:b9,platform:Mac OS X,
050000005769696d6f74652028313800,Wii U Pro Controller,a:b16,b:b15,back:b7,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b8,leftshoulder:b19,leftstick:b23,lefttrigger:b21,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b24,righttrigger:b22,rightx:a2,righty:a3,start:b6,x:b18,y:b17,platform:Mac OS X,
030000005e0400008e02000000000000,X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000c6240000045d000000000000,Xbox 360 Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000d102000000000000,Xbox One Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000dd02000000000000,Xbox One Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000e302000000000000,Xbox One Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000e002000000000000,Xbox Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Mac OS X,
030000005e040000e002000003090000,Xbox Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Mac OS X,
030000005e040000ea02000000000000,Xbox Wireless Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000fd02000003090000,Xbox Wireless Controller,a:b0,b:b1,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X,
03000000172700004431000029010000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Mac OS X,
03000000120c0000100e000000010000,ZEROPLUS P4 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
# Linux
03000000022000000090000011010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
05000000203800000900000000010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
05000000c82d00002038000000010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
03000000c82d00000190000011010000,8Bitdo NES30 Pro 8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
05000000102800000900000000010000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux,
05000000c82d00003028000000010000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux,
05000000a00500003232000001000000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux,
05000000a00500003232000008010000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux,
030000006f0e00003901000000430000,Afterglow Prismatic Wired Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006f0e00003901000020060000,Afterglow Wired Controller for Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000100000008200000011010000,Akishop Customs PS360+ v1.66,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
05000000050b00000045000031000000,ASUS Gamepad,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Linux,
03000000666600006706000000010000,boom PSX to PC Converter,a:b2,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a2,righty:a3,start:b11,x:b3,y:b0,platform:Linux,
03000000e82000006058000001010000,Cideko AK08b,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
03000000260900008888000000010000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a5,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Linux,
03000000a306000022f6000011010000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Linux,
03000000b40400000a01000000010000,CYPRESS USB Gamepad,a:b0,b:b1,back:b5,guide:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Linux,
03000000790000000600000010010000,DragonRise Inc. Generic USB Joystick,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b3,y:b0,platform:Linux,
030000006f0e00003001000001010000,EA Sports PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000341a000005f7000010010000,GameCube {HuiJia USB box},a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Linux,
0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
030000006f0e00000104000000010000,Gamestop Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006f0e00001304000000010000,Generic X-Box pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:a0,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:a3,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006f0e00001f01000000010000,Generic X-Box pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000f0250000c183000010010000,Goodbetterbest Ltd USB Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
0300000079000000d418000000010000,GPD Win 2 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000280400000140000000010000,Gravis GamePad Pro USB ,a:b1,b:b2,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
030000008f0e00000610000000010000,GreenAsia Electronics 4Axes 12Keys GamePad ,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a3,righty:a2,start:b11,x:b3,y:b0,platform:Linux,
030000008f0e00000300000010010000,GreenAsia Inc. USB Joystick,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
030000008f0e00001200000010010000,GreenAsia Inc. USB Joystick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux,
0500000047532067616d657061640000,GS gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
06000000adde0000efbe000002010000,Hidromancer Game Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000d81400000862000011010000,HitBox (PS3/PC) Analog Mode,a:b1,b:b2,back:b8,guide:b9,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b12,x:b0,y:b3,platform:Linux,
03000000c9110000f055000011010000,HJC Game GAMEPAD,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
030000000d0f00000d00000000010000,hori,a:b0,b:b6,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b3,leftx:b4,lefty:b5,rightshoulder:b7,start:b9,x:b1,y:b2,platform:Linux,
030000000d0f00001000000011010000,HORI CO. LTD. FIGHTING STICK 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00006a00000011010000,HORI CO. LTD. Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00006b00000011010000,HORI CO. LTD. Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00002200000011010000,HORI CO. LTD. REAL ARCADE Pro.V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00005f00000011010000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00005e00000011010000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000ad1b000001f5000033050000,Hori Pad EX Turbo 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000000d0f00006e00000011010000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00006600000011010000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00006700000001010000,HORIPAD ONE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000008f0e00001330000010010000,HuiJia SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b9,x:b3,y:b0,platform:Linux,
03000000830500006020000010010000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Linux,
050000006964726f69643a636f6e0000,idroid:con,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000b50700001503000010010000,impact,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux,
03000000fd0500000030000000010000,InterAct GoPad I-73000 (Fighting Game Layout),a:b3,b:b4,back:b6,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,start:b7,x:b0,y:b1,platform:Linux,
0500000049190000020400001b010000,Ipega PG-9069 - Bluetooth Gamepad,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b161,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux,
030000006e0500000320000010010000,JC-U3613M - DirectInput Mode,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Linux,
03000000300f00001001000010010000,Jess Tech Dual Analog Rumble Pad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux,
03000000ba2200002010000001010000,Jess Technology USB Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
030000006f0e00000103000000020000,Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006d04000019c2000010010000,Logitech Cordless RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d04000016c2000010010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d04000016c2000011010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d0400001dc2000014400000,Logitech F310 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006d0400001ec2000020200000,Logitech F510 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006d04000019c2000011010000,Logitech F710 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d0400001fc2000005030000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006d04000015c2000010010000,Logitech Logitech Extreme 3D,a:b0,b:b4,back:b6,guide:b8,leftshoulder:b9,leftstick:h0.8,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:h0.2,start:b7,x:b2,y:b5,platform:Linux,
030000006d04000018c2000010010000,Logitech RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d04000011c2000010010000,Logitech WingMan Cordless RumblePad,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b6,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b10,rightx:a3,righty:a4,start:b8,x:b3,y:b4,platform:Linux,
05000000380700006652000025010000,Mad Catz C.T.R.L.R ,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700005032000011010000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700005082000011010000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000ad1b00002ef0000090040000,Mad Catz Fightpad SFxT,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b2,y:b3,platform:Linux,
03000000380700008034000011010000,Mad Catz fightstick (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700008084000011010000,Mad Catz fightstick (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000380700008433000011010000,Mad Catz FightStick TE S+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700008483000011010000,Mad Catz FightStick TE S+ PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000380700001647000010040000,Mad Catz Wired Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000380700003847000090040000,Mad Catz Wired Xbox 360 Controller (SFIV),a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
03000000ad1b000016f0000090040000,Mad Catz Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000380700001888000010010000,MadCatz PC USB Wired Stick 8818,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700003888000010010000,MadCatz PC USB Wired Stick 8838,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:a0,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
0300000079000000d218000011010000,MAGIC-NS,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000790000004418000010010000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Linux,
03000000780000000600000010010000,Microntek USB Joystick,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux,
030000005e0400000e00000000010000,Microsoft SideWinder,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Linux,
030000005e0400008e02000004010000,Microsoft X-Box 360 pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400008e02000062230000,Microsoft X-Box 360 pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000e302000003020000,Microsoft X-Box One Elite pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000d102000001010000,Microsoft X-Box One pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000d102000003020000,Microsoft X-Box One pad v2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400008502000000010000,Microsoft X-Box pad (Japan),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux,
030000005e0400008902000021010000,Microsoft X-Box pad v2 (US),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux,
05000000d6200000ad0d000001000000,Moga Pro,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Linux,
03000000250900006688000000010000,MP-8866 Super Dual Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux,
030000001008000001e5000010010000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,platform:Linux,
050000007e0500000920000001000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
050000007e0500003003000001000000,Nintendo Wii Remote Pro Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Linux,
05000000010000000100000003000000,Nintendo Wiimote,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
030000000d0500000308000010010000,Nostromo n45 Dual Analog Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b12,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b10,x:b2,y:b3,platform:Linux,
03000000550900001072000011010000,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b13,leftshoulder:b4,leftstick:b8,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000451300000830000010010000,NYKO CORE,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000005e0400000202000000010000,Old Xbox pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux,
05000000362800000100000002010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2,platform:Linux,
05000000362800000100000003010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2,platform:Linux,
03000000ff1100003133000010010000,PC Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
030000006f0e00006401000001010000,PDP Battlefield One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000d62000006dca000011010000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000ff1100004133000010010000,PS2 Controller,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux,
03000000341a00003608000011010000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000004c0500006802000010010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
030000004c0500006802000010810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
030000004c0500006802000011010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
030000004c0500006802000011810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
050000004c0500006802000000010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:a12,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:a13,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
050000004c0500006802000000810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
050000004c0500006802000000800000,PS3 Controller (Bluetooth),a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
05000000504c415953544154494f4e00,PS3 Controller (Bluetooth),a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
060000004c0500006802000000010000,PS3 Controller (Bluetooth),a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
030000004c050000a00b000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004c050000a00b000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
030000004c050000c405000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004c050000c405000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
030000004c050000cc09000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004c050000cc09000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
050000004c050000c405000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
050000004c050000c405000000810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
050000004c050000cc09000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
050000004c050000cc09000000810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
03000000300f00001211000011010000,QanBa Arcade JoyStick,a:b2,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b5,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b6,start:b9,x:b1,y:b3,platform:Linux,
030000009b2800000300000001010000,raphnet.net 4nes4snes v1.5,a:b0,b:b4,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Linux,
030000008916000001fd000024010000,Razer Onza Classic Edition,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000008916000000fd000024010000,Razer Onza Tournament,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000321500000010000011010000,Razer RAIJU,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000c6240000045d000025010000,Razer Sabertooth,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000321500000009000011010000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
050000003215000000090000163a0000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
0300000032150000030a000001010000,Razer Wildcat,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000790000001100000010010000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Linux,
0300000000f000000300000000010000,RetroUSB.com RetroPad,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Linux,
0300000000f00000f100000000010000,RetroUSB.com Super RetroPort,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Linux,
030000006b140000010d000011010000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000006f0e00001e01000011010000,Rock Candy Gamepad for PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006f0e00004601000001010000,Rock Candy Wired Controller for Xbox One,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000a306000023f6000011010000,Saitek Cyborg V.1 Game Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Linux,
03000000a30600000cff000010010000,Saitek P2500 Force Rumble Pad,a:b2,b:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,x:b0,y:b1,platform:Linux,
03000000a30600000c04000011010000,Saitek P2900 Wireless Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b12,x:b0,y:b3,platform:Linux,
03000000a30600000901000000010000,Saitek P880,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,x:b0,y:b1,platform:Linux,
03000000a30600000b04000000010000,Saitek P990 Dual Analog Pad,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b8,x:b0,y:b3,platform:Linux,
03000000a306000018f5000010010000,Saitek PLC Saitek P3200 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Linux,
03000000c01600008704000011010000,Serial/Keyboard/Mouse/Joystick,a:b12,b:b10,back:b4,dpdown:b2,dpleft:b3,dpright:b1,dpup:b0,leftshoulder:b9,leftstick:b14,lefttrigger:b6,leftx:a1,lefty:a0,rightshoulder:b8,rightstick:b15,righttrigger:b7,rightx:a2,righty:a3,start:b5,x:b13,y:b11,platform:Linux,
03000000f025000021c1000010010000,ShanWan Gioteck PS3 Wired Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
03000000632500007505000010010000,SHANWAN PS3/PC Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
03000000250900000500000000010000,Sony PS2 pad with SmartJoy adapter,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux,
030000005e0400008e02000073050000,Speedlink TORID Wireless Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400008e02000020200000,SpeedLink XEOX Pro Analog Gamepad pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000de2800000112000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000de2800000211000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000de2800004211000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000de280000fc11000001000000,Steam Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
05000000de2800000212000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000de280000ff11000001000000,Steam Virtual Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000ad1b000038f0000090040000,Street Fighter IV FightStick TE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000666600000488000000010000,Super Joy Box 5 Pro,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux,
030000004f04000020b3000010010000,Thrustmaster 2 in 1 DT,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux,
030000004f04000015b3000010010000,Thrustmaster Dual Analog 4,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux,
030000004f04000023b3000000010000,Thrustmaster Dual Trigger 3-in-1,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004f04000000b3000010010000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Linux,
030000004f04000008d0000000010000,Thrustmaster Run N Drive Wireless,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004f04000009d0000000010000,Thrustmaster Run N Drive Wireless PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000bd12000015d0000010010000,Tomee SNES USB Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Linux,
03000000d814000007cd000011010000,Toodles 2008 Chimp PC/PS3,a:b0,b:b1,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b3,y:b2,platform:Linux,
03000000100800000100000010010000,Twin USB PS2 Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
03000000100800000300000010010000,USB Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
03000000790000001100000000010000,USB Gamepad1,a:b2,b:b1,back:b8,dpdown:a0,dpleft:a1,dpright:a2,dpup:a4,start:b9,platform:Linux,
05000000ac0500003232000001000000,VR-BOX,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux,
030000005e0400008e02000010010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400008e02000014010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400001907000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400009102000007010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000a102000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000a102000007010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
0000000058626f782033363020576900,Xbox 360 Wireless Controller,a:b0,b:b1,back:b14,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,guide:b7,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Linux,
030000005e040000a102000014010000,Xbox 360 Wireless Receiver (XBOX),a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
0000000058626f782047616d65706100,Xbox Gamepad (userspace driver),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
050000005e040000e002000003090000,Xbox One Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
050000005e040000fd02000003090000,Xbox One Wireless Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b16,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux,
03000000450c00002043000010010000,XEOX Gamepad SL-6556-BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
05000000172700004431000029010000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:a7,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Linux,
03000000c0160000e105000001010000,Xin-Mo Xin-Mo Dual Arcade,a:b4,b:b3,back:b6,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b9,leftshoulder:b2,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b1,y:b0,platform:Linux,
xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000120c0000100e000011010000,ZEROPLUS P4 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
# Android
64633436313965656664373634323364,Microsoft X-Box 360 pad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,x:b2,y:b3,platform:Android,
37336435666338653565313731303834,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android,
4e564944494120436f72706f72617469,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android,
61363931656135336130663561616264,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android,
35643031303033326130316330353564,PS4 Controller,a:b1,b:b17,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,leftstick:b4,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:+a4,rightx:a2,righty:a5,start:b16,x:b0,y:b2,platform:Android,
05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Android,
5477696e20555342204a6f7973746963,Twin USB Joystick,a:b22,b:b21,back:b28,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b26,leftstick:b30,lefttrigger:b24,leftx:a0,lefty:a1,rightshoulder:b27,rightstick:b31,righttrigger:b25,rightx:a3,righty:a2,start:b29,x:b23,y:b20,platform:Android,
34356136633366613530316338376136,Xbox Wireless Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b3,leftstick:b15,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b16,righttrigger:a5,rightx:a3,righty:a4,x:b17,y:b2,platform:Android,
# iOS
4d466947616d65706164010000000000,MFi Extended Gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:iOS,
4d466947616d65706164020000000000,MFi Gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,rightshoulder:b5,start:b6,x:b2,y:b3,platform:iOS,
05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:iOS,
+105 -29
View File
@@ -1,4 +1,5 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Dialogs 1.2
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import AppModel 1.0 import AppModel 1.0
@@ -12,29 +13,28 @@ GridView {
id: appGrid id: appGrid
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2 anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 5 anchors.topMargin: 20
anchors.rightMargin: 5 anchors.rightMargin: 5
anchors.bottomMargin: 5 anchors.bottomMargin: 5
cellWidth: 225; cellHeight: 350; cellWidth: 225; cellHeight: 385;
focus: true
// Cache delegates for 1000px in both directions to improve
// scrolling and resizing performance
cacheBuffer: 1000
// The StackView will trigger a visibility change when // The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged // we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible // routine to run, but only if we start as invisible
visible: false visible: false
function computerLost()
{
// Go back to the PC view on PC loss
stackView.pop()
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
// Start polling when this view is shown appModel.computerLost.connect(computerLost)
ComputerManager.startPolling()
} }
else { else {
// Stop polling when this view is not on top appModel.computerLost.disconnect(computerLost)
ComputerManager.stopPollingAsync()
} }
} }
@@ -47,18 +47,13 @@ GridView {
model: appModel model: appModel
delegate: Item { delegate: ItemDelegate {
width: 200; height: 300; width: 200; height: 335;
Component.onCompleted: {
if (model.running) {
appNameText.text = "<font color=\"green\">Running</font><font color=\"white\"> - </font>" + appNameText.text
}
}
Image { Image {
id: appIcon id: appIcon
anchors.horizontalCenter: parent.horizontalCenter; anchors.horizontalCenter: parent.horizontalCenter;
y: 20
source: model.boxart source: model.boxart
sourceSize { sourceSize {
width: 150 width: 150
@@ -66,31 +61,112 @@ GridView {
} }
} }
Image {
id: runningIcon
anchors.centerIn: appIcon
visible: model.running
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
sourceSize {
width: 75
height: 75
}
}
Text { Text {
id: appNameText id: appNameText
text: model.name text: model.name
color: "white" color: "white"
width: parent.width width: parent.width
height: 100 height: 125
anchors.top: appIcon.bottom anchors.top: appIcon.bottom
font.pointSize: 26 font.pointSize: 22
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
elide: Text.ElideRight
}
function launchOrResumeSelectedApp()
{
var runningIndex = appModel.getRunningAppIndex()
if (runningIndex >= 0 && runningIndex !== index) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.nextAppName = model.name
quitAppDialog.nextAppIndex = index
quitAppDialog.open()
return
}
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {"appName": model.name, "session": appModel.createSessionForApp(index)})
stackView.push(segue)
}
onClicked: {
// Nothing is running or this app is running
launchOrResumeSelectedApp()
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: { onClicked: {
// TODO: Check if a different game is running // Right click
appContextMenu.open()
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView)
segue.appname = model.name
segue.session = appModel.createSessionForApp(index)
stackView.push(segue)
} }
} }
Menu {
id: appContextMenu
MenuItem {
text: model.running ? "Resume Game" : "Launch Game"
onTriggered: {
appContextMenu.close()
launchOrResumeSelectedApp()
}
height: visible ? implicitHeight : 0
}
MenuItem {
text: "Quit Game"
onTriggered: {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
visible: model.running
height: visible ? implicitHeight : 0
}
}
}
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : ""
property bool segueToStream : false
property string nextAppName: ""
property int nextAppIndex: 0
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
onYes: {
var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName}
if (segueToStream) {
// Store the session and app name if we're going to stream after
// successfully quitting the old app.
params.nextAppName = nextAppName
params.nextSession = appModel.createSessionForApp(nextAppIndex)
}
else {
params.nextAppName = null
params.nextSession = null
}
stackView.push(component.createObject(stackView, params))
// Trigger the quit after pushing the quit segue on screen
appModel.quitRunningApp()
}
} }
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
+134 -81
View File
@@ -1,11 +1,13 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3 import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import ComputerModel 1.0 import ComputerModel 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import StreamingPreferences 1.0
GridView { GridView {
property ComputerModel computerModel : createModel() property ComputerModel computerModel : createModel()
@@ -13,11 +15,10 @@ GridView {
id: pcGrid id: pcGrid
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2 anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 5 anchors.topMargin: 20
anchors.rightMargin: 5 anchors.rightMargin: 5
anchors.bottomMargin: 5 anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350; cellWidth: 350; cellHeight: 350;
focus: true
objectName: "Computers" objectName: "Computers"
// The StackView will trigger a visibility change when // The StackView will trigger a visibility change when
@@ -25,19 +26,19 @@ GridView {
// routine to run, but only if we start as invisible // routine to run, but only if we start as invisible
visible: false visible: false
StreamingPreferences {
id: prefs
}
Component.onCompleted: { Component.onCompleted: {
// Setup signals on CM // Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete) ComputerManager.computerAddCompleted.connect(addComplete)
}
onVisibleChanged: { if (prefs.isRunningWayland()) {
if (visible) { waylandDialog.open()
// Start polling when this view is shown
ComputerManager.startPolling()
} }
else { else if (!prefs.hasAnyHardwareAcceleration()) {
// Stop polling when this view is not top-most noHwDecoderDialog.open()
ComputerManager.stopPollingAsync()
} }
} }
@@ -46,11 +47,8 @@ GridView {
// Close the PIN dialog // Close the PIN dialog
pairDialog.close() pairDialog.close()
// Start polling again
ComputerManager.startPolling()
// Display a failed dialog if we got an error // Display a failed dialog if we got an error
if (error !== null) { if (error !== undefined) {
errorDialog.text = error errorDialog.text = error
errorDialog.open() errorDialog.open()
} }
@@ -59,7 +57,7 @@ GridView {
function addComplete(success) function addComplete(success)
{ {
if (!success) { if (!success) {
errorDialog.text = "Unable to connect to the specified PC. Ensure GameStream is enabled in GeForce Experience." errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
errorDialog.open() errorDialog.open()
} }
} }
@@ -72,14 +70,14 @@ GridView {
return model return model
} }
model: createModel() model: computerModel
delegate: Item { delegate: ItemDelegate {
width: 300; height: 300; width: 300; height: 300;
Image { Image {
id: pcIcon id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter; anchors.horizontalCenter: parent.horizontalCenter
source: { source: {
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg" model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
} }
@@ -89,6 +87,30 @@ GridView {
} }
} }
Image {
// TODO: Tooltip
id: stateIcon
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -10
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
sourceSize {
width: 75
height: 75
}
}
BusyIndicator {
id: statusUnknownSpinner
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -10
width: 75
height: 75
visible: !model.addPc && model.statusUnknown
}
Text { Text {
id: pcNameText id: pcNameText
text: model.name text: model.name
@@ -101,76 +123,64 @@ GridView {
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
Text { Menu {
function getStatusText(model) id: pcContextMenu
{ MenuItem {
if (model.online) { text: "Wake PC"
var text = "<font color=\"green\">Online</font>" onTriggered: computerModel.wakeComputer(index)
text += "<font color=\"white\"> - </font>" visible: !model.addPc && !model.online && model.wakeable
if (model.paired) { height: visible ? implicitHeight : 0
text += "<font color=\"skyblue\">Paired</font>" }
} MenuItem {
else if (model.busy) { text: "Delete PC"
text += "<font color=\"red\">Busy</font>" onTriggered: {
} deletePcDialog.pcIndex = index
else { // get confirmation first, actual closing is called from the dialog
text += "<font color=\"red\">Not Paired</font>" deletePcDialog.open()
}
return text
}
else {
return "<font color=\"red\">Offline</font>";
} }
} }
}
id: pcPairedText onClicked: {
text: getStatusText(model) if (model.addPc) {
visible: !model.addPc addPcDialog.open()
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
stackView.push(appView)
}
else {
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
width: parent.width // Kick off pairing in the background
anchors.top: pcNameText.bottom computerModel.pairComputer(index, pin)
font.pointSize: 24
horizontalAlignment: Text.AlignHCenter // Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
}
} else if (!model.online) {
pcContextMenu.open()
}
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton;
onClicked: { onClicked: {
if (model.addPc) { // right click
addPcDialog.open() if (!model.addPc) { // but only for actual PCs, not the add-pc option
} pcContextMenu.open()
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView)
appView.computerIndex = index
appView.objectName = model.name
stackView.push(appView)
}
else {
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Stop polling, since pairing may make GFE unresponsive
ComputerManager.stopPollingAsync()
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
// Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
}
}
else {
// TODO: Wake on LAN and delete PC options
} }
} }
} }
@@ -181,7 +191,37 @@ GridView {
// don't allow edits to the rest of the window while open // don't allow edits to the rest of the window while open
modality:Qt.WindowModal modality:Qt.WindowModal
icon: StandardIcon.Critical icon: StandardIcon.Critical
standardButtons: StandardButton.Ok standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
// will arrive here by forgetting to enable GameStream or not forwarding ports.
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
}
}
MessageDialog {
id: noHwDecoderDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration. " +
"Click the Help button for more information on solving this problem."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
MessageDialog {
id: waylandDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
"Please switch to an X session for optimal performance."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
} }
MessageDialog { MessageDialog {
@@ -196,6 +236,19 @@ GridView {
} }
} }
MessageDialog {
id: deletePcDialog
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property int pcIndex : -1;
text:"Are you sure you want to remove this PC?"
standardButtons: StandardButton.Yes | StandardButton.No
onYes: {
console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex);
}
}
Dialog { Dialog {
id: addPcDialog id: addPcDialog
property string label: "Enter the IP address of your GameStream PC" property string label: "Enter the IP address of your GameStream PC"
+78
View File
@@ -0,0 +1,78 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
Item {
property string appName
property Session nextSession : null
property string nextAppName : ""
property string stageText : "Quitting " + appName + "..."
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function quitAppCompleted(error)
{
// Display a failed dialog if we got an error
if (error !== undefined) {
errorDialog.text = error
errorDialog.open()
}
// Exit this view
stackView.pop()
// If we're supposed to launch another game after this, do so now
if (error === undefined && nextSession !== null) {
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
stackView.push(segue)
}
}
onVisibleChanged: {
if (visible) {
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
}
else {
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
}
}
Row {
anchors.centerIn: parent
spacing: 5
BusyIndicator {
id: stageSpinner
}
Label {
id: stageLabel
height: stageSpinner.height
text: stageText
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
color: "white"
}
}
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
}
+451 -120
View File
@@ -2,6 +2,7 @@ import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import ComputerManager 1.0
ScrollView { ScrollView {
id: settingsPage id: settingsPage
@@ -16,14 +17,13 @@ ScrollView {
} }
Column { Column {
x: 10 padding: 10
y: 10 id: settingsColumn1
width: settingsPage.width width: settingsPage.width / 2 - padding
height: 400
GroupBox { GroupBox {
id: basicSettingsGroupBox id: basicSettingsGroupBox
width: (parent.width - 20) width: (parent.width - 2 * parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Basic Settings</font>" title: "<font color=\"skyblue\">Basic Settings</font>"
font.pointSize: 12 font.pointSize: 12
@@ -35,7 +35,7 @@ ScrollView {
Label { Label {
width: parent.width width: parent.width
id: resFPStitle id: resFPStitle
text: qsTr("Resolution and FPS target") text: qsTr("Resolution and FPS")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white" color: "white"
@@ -44,84 +44,208 @@ ScrollView {
Label { Label {
width: parent.width width: parent.width
id: resFPSdesc id: resFPSdesc
text: qsTr("Setting values too high for your device may cause lag or crashes") text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors.")
font.pointSize: 9 font.pointSize: 9
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white" color: "white"
} }
ComboBox { Row {
// ignore setting the index at first, and actually set it when the component is loaded spacing: 5
Component.onCompleted: {
// load the saved width/height/fps, and iterate through the ComboBox until a match is found ComboBox {
// set it to that index. // ignore setting the index at first, and actually set it when the component is loaded
var saved_width = prefs.width Component.onCompleted: {
var saved_height = prefs.height // Add native resolutions for all attached displays
var saved_fps = prefs.fps var done = false
currentIndex = 0 for (var displayIndex = 0; !done; displayIndex++) {
for(var i = 0; i < resolutionComboBox.count; i++) { for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
var el_width = parseInt(resolutionListModel.get(i).video_width); var screenRect;
var el_height = parseInt(resolutionListModel.get(i).video_height);
var el_fps = parseInt(resolutionListModel.get(i).video_fps); // Some platforms have different desktop resolutions
if(saved_width === el_width && // and native resolutions (like macOS with Retina displays)
saved_height === el_height && if (displayResIndex == 0) {
saved_fps === el_fps) { screenRect = prefs.getDesktopResolution(displayIndex)
currentIndex = i }
} else {
screenRect = prefs.getNativeResolution(displayIndex)
}
if (screenRect.width === 0) {
// Exceeded max count of displays
done = true
break
}
var indexToAdd = 0
for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
if (screenRect.width === existing_width && screenRect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
"video_width": ""+screenRect.width,
"video_height": ""+screenRect.height
})
}
}
}
// load the saved width/height, and iterate through the ComboBox until a match is found
// and set it to that index.
var saved_width = prefs.width
var saved_height = prefs.height
currentIndex = 0
for (var i = 0; i < resolutionListModel.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width);
var el_height = parseInt(resolutionListModel.get(i).video_height);
// Pick the highest value lesser or equal to the saved resolution
if (saved_width * saved_height >= el_width * el_height) {
currentIndex = i
}
}
// Persist the selected value
activated(currentIndex)
}
id: resolutionComboBox
width: 200
textRole: "text"
model: ListModel {
id: resolutionListModel
// Other elements may be added at runtime
// based on attached display resolution
ListElement {
text: "720p"
video_width: "1280"
video_height: "720"
}
ListElement {
text: "1080p"
video_width: "1920"
video_height: "1080"
}
ListElement {
text: "1440p"
video_width: "2560"
video_height: "1440"
}
ListElement {
text: "4K"
video_width: "3840"
video_height: "2160"
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
var selectedWidth = parseInt(resolutionListModel.get(currentIndex).video_width)
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
// Only modify the bitrate if the values actually changed
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
prefs.width = selectedWidth
prefs.height = selectedHeight
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
}
} }
} }
id: resolutionComboBox ComboBox {
width: Math.min(bitrateDesc.implicitWidth, parent.width) function createModel() {
font.pointSize: 9 var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
textRole: "text"
model: ListModel {
id: resolutionListModel
ListElement {
text: "720p 30 FPS"
video_width: "1280"
video_height: "720"
video_fps: "30"
}
ListElement {
text: "720p 60 FPS"
video_width: "1280"
video_height: "720"
video_fps: "60"
}
ListElement {
text: "1080p 30 FPS"
video_width: "1920"
video_height: "1080"
video_fps: "30"
}
ListElement {
text: "1080p 60 FPS"
video_width: "1920"
video_height: "1080"
video_fps: "60"
}
ListElement {
text: "4K 30 FPS"
video_width: "3840"
video_height: "2160"
video_fps: "30"
}
ListElement {
text: "4K 60 FPS"
video_width: "3840"
video_height: "2160"
video_fps: "60"
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
prefs.fps = parseInt(resolutionListModel.get(currentIndex).video_fps)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps); // Get the max supported FPS on this system
slider.value = prefs.bitrateKbps var max_fps = prefs.getMaximumStreamingFrameRate();
// Default entries
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
// Add unsupported FPS values that come before the display max FPS
if (prefs.unsupportedFps) {
if (max_fps > 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
}
if (max_fps > 120) {
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
}
}
// Use 64 as the cutoff for adding a separate option to
// handle wonky displays that report just over 60 Hz.
if (max_fps > 64) {
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
}
// Add unsupported FPS values that come after the display max FPS
if (prefs.unsupportedFps) {
if (max_fps < 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
}
if (max_fps < 120) {
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
}
}
return fpsListModel
}
function reinitialize() {
model = createModel()
var saved_fps = prefs.fps
currentIndex = 0
for (var i = 0; i < model.count; i++) {
var el_fps = parseInt(model.get(i).video_fps);
// Pick the highest value lesser or equal to the saved FPS
if (saved_fps >= el_fps) {
currentIndex = i
}
}
// Persist the selected value
activated(currentIndex)
}
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
reinitialize()
}
id: fpsComboBox
textRole: "text"
width: 250
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
var selectedFps = parseInt(model.get(currentIndex).video_fps)
// Only modify the bitrate if the values actually changed
if (prefs.fps !== selectedFps) {
prefs.fps = selectedFps
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
}
}
} }
} }
@@ -151,7 +275,7 @@ ScrollView {
stepSize: 500 stepSize: 500
from : 500 from : 500
to: 100000 to: 150000
snapMode: "SnapOnRelease" snapMode: "SnapOnRelease"
width: Math.min(bitrateDesc.implicitWidth, parent.width) width: Math.min(bitrateDesc.implicitWidth, parent.width)
@@ -162,13 +286,60 @@ ScrollView {
} }
} }
Label {
width: parent.width
id: windowModeTitle
text: qsTr("Display mode")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var savedWm = prefs.windowMode
currentIndex = 0
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
if (savedWm === thisWm) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: windowModeComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: windowModeListModel
ListElement {
text: "Full-screen (Recommended)"
val: StreamingPreferences.WM_FULLSCREEN
}
ListElement {
text: "Borderless windowed"
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
}
ListElement {
text: "Windowed"
val: StreamingPreferences.WM_WINDOWED
}
}
onActivated: {
prefs.windowMode = windowModeListModel.get(currentIndex).val
}
}
CheckBox { CheckBox {
id: fullScreenCheck id: vsyncCheck
text: "<font color=\"white\">Full-screen</font>" text: "<font color=\"white\">Enable V-Sync</font>"
font.pointSize: 12 font.pointSize: 12
checked: prefs.fullScreen checked: prefs.enableVsync
onCheckedChanged: { onCheckedChanged: {
prefs.fullScreen = checked prefs.enableVsync = checked
} }
} }
} }
@@ -177,7 +348,7 @@ ScrollView {
GroupBox { GroupBox {
id: audioSettingsGroupBox id: audioSettingsGroupBox
width: (parent.width - 20) width: (parent.width - 2 * parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Audio Settings</font>" title: "<font color=\"skyblue\">Audio Settings</font>"
font.pointSize: 12 font.pointSize: 12
@@ -186,29 +357,72 @@ ScrollView {
anchors.fill: parent anchors.fill: parent
spacing: 5 spacing: 5
CheckBox { Label {
id: surroundSoundCheck width: parent.width
text: "<font color=\"white\">Enable 5.1 surround sound</font>" id: resAudioTitle
font.pointSize: 12 text: qsTr("Audio configuration")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
// the backend actually supports auto/stereo/5.1. We'll expose stereo/5.1 ComboBox {
checked: prefs.audioConfig === StreamingPreferences.AC_FORCE_SURROUND // ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_audio = prefs.audioConfig
currentIndex = 0
for (var i = 0; i < audioListModel.count; i++) {
var el_audio = audioListModel.get(i).val;
if (saved_audio === el_audio) {
currentIndex = i
break
}
}
activated(currentIndex)
}
onCheckedChanged: { id: audioComboBox
prefs.audioConfig = checked ? StreamingPreferences.AC_FORCE_SURROUND : StreamingPreferences.AC_FORCE_STEREO width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: audioListModel
ListElement {
text: "Autodetect"
val: StreamingPreferences.AC_AUTO
}
ListElement {
text: "Stereo"
val: StreamingPreferences.AC_FORCE_STEREO
}
ListElement {
text: "5.1 surround sound"
val: StreamingPreferences.AC_FORCE_SURROUND
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.audioConfig = audioListModel.get(currentIndex).val
} }
} }
} }
} }
}
Column {
padding: 10
anchors.left: settingsColumn1.right
id: settingsColumn2
width: settingsPage.width / 2 - padding
GroupBox { GroupBox {
id: gamepadSettingsGroupBox id: gamepadSettingsGroupBox
width: (parent.width - 20) width: (parent.width - parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Gamepad Settings</font>" title: "<font color=\"skyblue\">Input Settings</font>"
font.pointSize: 12 font.pointSize: 12
Column { Row {
anchors.fill: parent anchors.fill: parent
spacing: 5 spacing: 5
@@ -221,49 +435,35 @@ ScrollView {
prefs.multiController = checked prefs.multiController = checked
} }
} }
CheckBox {
id: mouseEmulationCheck
text: "<font color=\"white\">Mouse emulation via gamepad</font>"
font.pointSize: 12
// TODO: make this actually do anything
}
}
}
GroupBox {
id: onScreenControlsGroupBox
width: (parent.width - 20)
padding: 12
title: "<font color=\"skyblue\">On-screen Controls Settings</font>"
font.pointSize: 12
Column {
anchors.fill: parent
spacing: 5
CheckBox { CheckBox {
id: onScreenControlsCheck id: mouseAccelerationCheck
text: "<font color=\"white\">Show on-screen controls</font>" text: "<font color=\"white\">Enable mouse acceleration</font>"
font.pointSize: 12 font.pointSize: 12
// TODO: make this actually do anything checked: prefs.mouseAcceleration
onCheckedChanged: {
prefs.mouseAcceleration = checked
}
} }
} }
} }
GroupBox { GroupBox {
id: hostSettingsGroupBox id: hostSettingsGroupBox
width: (parent.width - 20) width: (parent.width - parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Host Settings</font>" title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12 font.pointSize: 12
Column { Row {
anchors.fill: parent anchors.fill: parent
spacing: 5 spacing: 5
CheckBox { CheckBox {
id: optimizeGameSettingsCheck id: optimizeGameSettingsCheck
text: "<font color=\"white\">Optimize game settings</font>" text: "<font color=\"white\">Optimize game settings</font>"
// HACK: Match width of the other checkbox to make the UI not look bad
width: multiControllerCheck.width
font.pointSize: 12 font.pointSize: 12
checked: prefs.gameOptimizations checked: prefs.gameOptimizations
onCheckedChanged: { onCheckedChanged: {
@@ -285,7 +485,7 @@ ScrollView {
GroupBox { GroupBox {
id: advancedSettingsGroupBox id: advancedSettingsGroupBox
width: (parent.width - 20) width: (parent.width - parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Advanced Settings</font>" title: "<font color=\"skyblue\">Advanced Settings</font>"
font.pointSize: 12 font.pointSize: 12
@@ -294,10 +494,141 @@ ScrollView {
anchors.fill: parent anchors.fill: parent
spacing: 5 spacing: 5
Label {
width: parent.width
id: resVDSTitle
text: qsTr("Video decoder")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vds = prefs.videoDecoderSelection
currentIndex = 0
for (var i = 0; i < decoderListModel.count; i++) {
var el_vds = decoderListModel.get(i).val;
if (saved_vds === el_vds) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: decoderComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: decoderListModel
ListElement {
text: "Automatic (Recommended)"
val: StreamingPreferences.VDS_AUTO
}
ListElement {
text: "Force software decoding"
val: StreamingPreferences.VDS_FORCE_SOFTWARE
}
ListElement {
text: "Force hardware decoding"
val: StreamingPreferences.VDS_FORCE_HARDWARE
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
}
}
Label {
width: parent.width
id: resVCCTitle
text: qsTr("Video codec")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vcc = prefs.videoCodecConfig
currentIndex = 0
for(var i = 0; i < codecListModel.count; i++) {
var el_vcc = codecListModel.get(i).val;
if (saved_vcc === el_vcc) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: codecComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: codecListModel
ListElement {
text: "Automatic (Recommended)"
val: StreamingPreferences.VCC_AUTO
}
ListElement {
text: "Force H.264"
val: StreamingPreferences.VCC_FORCE_H264
}
ListElement {
text: "Force HEVC"
val: StreamingPreferences.VCC_FORCE_HEVC
}
// HDR seems to be broken in GFE 3.14.1, and even when that's fixed
// we'll probably need to gate this feature on OS support in our
// renderers.
/* ListElement {
text: "Force HEVC HDR"
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
} */
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.videoCodecConfig = codecListModel.get(currentIndex).val
}
}
CheckBox { CheckBox {
id: neverDropFramesCheck id: unlockUnsupportedFps
text: "<font color=\"white\">Never drop frames</font>" text: "<font color=\"white\">Unlock unsupported FPS options</font>"
font.pointSize: 12 font.pointSize: 12
checked: prefs.unsupportedFps
onCheckedChanged: {
prefs.unsupportedFps = checked
// The selectable FPS values depend on whether
// this option is enabled or not
fpsComboBox.reinitialize()
}
}
CheckBox {
id: enableMdns
text: "<font color=\"white\">Automatically find PCs on the local network (Recommended)</font>"
font.pointSize: 12
checked: prefs.enableMdns
onCheckedChanged: {
prefs.enableMdns = checked
// We must save the updated preference to ensure
// ComputerManager can observe the change internally.
prefs.save()
// Restart polling so the mDNS change takes effect
if (window.pollingActive) {
ComputerManager.stopPollingAsync()
ComputerManager.startPolling()
}
}
} }
} }
} }
+28 -6
View File
@@ -1,13 +1,14 @@
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3 import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2
import Session 1.0 import Session 1.0
Item { Item {
property Session session property Session session
property string appname property string appName
property string stageText : "Starting " + appname + "..." property string stageText : "Starting " + appName + "..."
anchors.fill: parent anchors.fill: parent
@@ -47,7 +48,13 @@ Item {
function displayLaunchWarning(text) function displayLaunchWarning(text)
{ {
// TODO: toast // This toast appears for 3 seconds, just shorter than how long
// Session will wait for it to be displayed. This gives it time
// to transition to invisible before continuing.
var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
toast.text = text
toast.timeout = 3000
toast.visible = true
} }
onVisibleChanged: { onVisibleChanged: {
@@ -63,7 +70,7 @@ Item {
session.displayLaunchWarning.connect(displayLaunchWarning) session.displayLaunchWarning.connect(displayLaunchWarning)
// Run the streaming session to completion // Run the streaming session to completion
session.exec() session.exec(Screen.virtualX, Screen.virtualY)
// Show the Qt window again after streaming // Show the Qt window again after streaming
window.visible = true window.visible = true
@@ -97,10 +104,25 @@ Item {
} }
} }
Label {
text: "Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
anchors.bottom: parent.bottom
anchors.bottomMargin: 50
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: 18
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
color: "white"
}
MessageDialog { MessageDialog {
id: errorDialog id: errorDialog
modality:Qt.WindowModal modality:Qt.WindowModal
icon: StandardIcon.Critical icon: StandardIcon.Critical
standardButtons: StandardButton.Ok standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
}
} }
} }
+49 -5
View File
@@ -19,6 +19,32 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
m_CurrentGameId = m_Computer->currentGameId; m_CurrentGameId = m_Computer->currentGameId;
} }
int AppModel::getRunningAppIndex()
{
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return i;
}
}
}
return -1;
}
QString AppModel::getRunningAppName()
{
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return m_Apps[i].name;
}
}
}
return nullptr;
}
Session* AppModel::createSessionForApp(int appIndex) Session* AppModel::createSessionForApp(int appIndex)
{ {
Q_ASSERT(appIndex < m_Apps.count()); Q_ASSERT(appIndex < m_Apps.count());
@@ -70,6 +96,11 @@ QHash<int, QByteArray> AppModel::roleNames() const
return names; return names;
} }
void AppModel::quitRunningApp()
{
m_ComputerManager->quitRunningApp(m_Computer);
}
void AppModel::handleComputerStateChanged(NvComputer* computer) void AppModel::handleComputerStateChanged(NvComputer* computer)
{ {
// Ignore updates for computers that aren't ours // Ignore updates for computers that aren't ours
@@ -77,6 +108,14 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
return; return;
} }
// If the computer has gone offline or we've been unpaired,
// signal the UI so we can go back to the PC view.
if (m_Computer->state == NvComputer::CS_OFFLINE ||
m_Computer->pairState == NvComputer::PS_NOT_PAIRED) {
emit computerLost();
return;
}
// First, process additions/removals from the app list. This // First, process additions/removals from the app list. This
// is required because the new game may now be running, so // is required because the new game may now be running, so
// we can't check that first. // we can't check that first.
@@ -123,10 +162,15 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
Q_ASSERT(computer == m_Computer); Q_ASSERT(computer == m_Computer);
int index = m_Apps.indexOf(app); int index = m_Apps.indexOf(app);
Q_ASSERT(index >= 0);
// Let our view know the box art data has changed for this app // Make sure we're not delivering a callback to an app that's already been removed
emit dataChanged(createIndex(index, 0), if (index >= 0) {
createIndex(index, 0), // Let our view know the box art data has changed for this app
QVector<int>() << BoxArtRole); emit dataChanged(createIndex(index, 0),
createIndex(index, 0),
QVector<int>() << BoxArtRole);
}
else {
qWarning() << "App not found for box art callback:" << app.name;
}
} }
+9
View File
@@ -25,6 +25,12 @@ public:
Q_INVOKABLE Session* createSessionForApp(int appIndex); Q_INVOKABLE Session* createSessionForApp(int appIndex);
Q_INVOKABLE int getRunningAppIndex();
Q_INVOKABLE QString getRunningAppName();
Q_INVOKABLE void quitRunningApp();
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent) const override;
@@ -36,6 +42,9 @@ private slots:
void handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl image); void handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl image);
signals:
void computerLost();
private: private:
NvComputer* m_Computer; NvComputer* m_Computer;
BoxArtManager m_BoxArtManager; BoxArtManager m_BoxArtManager;
+27 -3
View File
@@ -1,5 +1,7 @@
#include "computermodel.h" #include "computermodel.h"
#include <QThreadPool>
ComputerModel::ComputerModel(QObject* object) ComputerModel::ComputerModel(QObject* object)
: QAbstractListModel(object) {} : QAbstractListModel(object) {}
@@ -46,8 +48,12 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->pairState == NvComputer::PS_PAIRED; return computer->pairState == NvComputer::PS_PAIRED;
case BusyRole: case BusyRole:
return computer->currentGameId != 0; return computer->currentGameId != 0;
case WakeableRole:
return !computer->macAddress.isEmpty();
case AddPcRole: case AddPcRole:
return false; return false;
case StatusUnknownRole:
return computer->state == NvComputer::CS_UNKNOWN;
default: default:
return QVariant(); return QVariant();
} }
@@ -74,6 +80,8 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[PairedRole] = "paired"; names[PairedRole] = "paired";
names[BusyRole] = "busy"; names[BusyRole] = "busy";
names[AddPcRole] = "addPc"; names[AddPcRole] = "addPc";
names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown";
return names; return names;
} }
@@ -113,11 +121,27 @@ void ComputerModel::deleteComputer(int computerIndex)
endRemoveRows(); endRemoveRows();
} }
bool ComputerModel::wakeComputer(int computerIndex) class DeferredWakeHostTask : public QRunnable
{
public:
DeferredWakeHostTask(NvComputer* computer)
: m_Computer(computer) {}
void run()
{
m_Computer->wake();
}
private:
NvComputer* m_Computer;
};
void ComputerModel::wakeComputer(int computerIndex)
{ {
Q_ASSERT(computerIndex < m_Computers.count()); Q_ASSERT(computerIndex < m_Computers.count());
return m_Computers[computerIndex]->wake(); DeferredWakeHostTask* wakeTask = new DeferredWakeHostTask(m_Computers[computerIndex]);
QThreadPool::globalInstance()->start(wakeTask);
} }
void ComputerModel::pairComputer(int computerIndex, QString pin) void ComputerModel::pairComputer(int computerIndex, QString pin)
@@ -129,7 +153,7 @@ void ComputerModel::pairComputer(int computerIndex, QString pin)
void ComputerModel::handlePairingCompleted(NvComputer*, QString error) void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
{ {
emit pairingCompleted(error.isNull() ? QVariant() : error); emit pairingCompleted(error.isEmpty() ? QVariant() : error);
} }
void ComputerModel::handleComputerStateChanged(NvComputer* computer) void ComputerModel::handleComputerStateChanged(NvComputer* computer)
+3 -1
View File
@@ -13,6 +13,8 @@ class ComputerModel : public QAbstractListModel
OnlineRole, OnlineRole,
PairedRole, PairedRole,
BusyRole, BusyRole,
WakeableRole,
StatusUnknownRole,
AddPcRole AddPcRole
}; };
@@ -32,7 +34,7 @@ public:
Q_INVOKABLE void pairComputer(int computerIndex, QString pin); Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
Q_INVOKABLE bool wakeComputer(int computerIndex); Q_INVOKABLE void wakeComputer(int computerIndex);
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex); Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
+119 -27
View File
@@ -1,10 +1,16 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import QtQuick.Controls.Material 2.1 import QtQuick.Controls.Material 2.1
import ComputerManager 1.0
import AutoUpdateChecker 1.0
ApplicationWindow { ApplicationWindow {
property bool pollingActive: false
id: window id: window
visible: true visible: true
width: 1280 width: 1280
@@ -19,6 +25,24 @@ ApplicationWindow {
anchors.fill: parent anchors.fill: parent
} }
onVisibilityChanged: {
// We don't want to just use 'active' here because that will stop polling if
// we lose focus, which might be overzealous for users with multiple screens
// where we may be clearly visible on the other display. Ideally we'll poll
// only if the window is visible to the user (not if obscured by other windows),
// but it seems difficult to do this portably.
var shouldPoll = visibility !== Window.Minimized && visibility !== Window.Hidden
if (shouldPoll && !pollingActive) {
ComputerManager.startPolling()
pollingActive = true
}
else if (!shouldPoll && pollingActive) {
ComputerManager.stopPollingAsync()
pollingActive = false
}
}
function navigateTo(url, objectName) function navigateTo(url, objectName)
{ {
var existingItem = stackView.find(function(item, index) { var existingItem = stackView.find(function(item, index) {
@@ -37,6 +61,8 @@ ApplicationWindow {
header: ToolBar { header: ToolBar {
id: toolBar id: toolBar
anchors.topMargin: 5
anchors.bottomMargin: 5
RowLayout { RowLayout {
spacing: 20 spacing: 20
@@ -46,14 +72,19 @@ ApplicationWindow {
// Only make the button visible if the user has navigated somewhere. // Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1 visible: stackView.depth > 1
icon.source: "qrc:/res/arrow_left.svg" // FIXME: We're using an Image here rather than icon.source because
onClicked: stackView.pop() // icons don't work on Qt 5.9 LTS.
Image {
Menu { source: "qrc:/res/arrow_left.svg"
id: backButton anchors.centerIn: parent
x: parent.width - width sourceSize {
transformOrigin: Menu.TopRight // The icon should be square so use the height as the width too
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
} }
onClicked: stackView.pop()
} }
Label { Label {
@@ -67,38 +98,99 @@ ApplicationWindow {
} }
ToolButton { ToolButton {
icon.source: "qrc:/res/question_mark.svg" property string browserUrl: ""
id: updateButton
Image {
source: "qrc:/res/update.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Update available for Moonlight"
// Invisible until we get a callback notifying us that
// an update is available
visible: false
onClicked: Qt.openUrlExternally(browserUrl);
function updateAvailable(url)
{
updateButton.browserUrl = url
updateButton.visible = true
}
Component.onCompleted: {
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
AutoUpdateChecker.start()
}
}
ToolButton {
Image {
source: "qrc:/res/question_mark.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Help"
// TODO need to make sure browser is brought to foreground. // TODO need to make sure browser is brought to foreground.
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"); onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
Menu {
id: helpButton
x: parent.width
transformOrigin: Menu.TopRight
}
} }
ToolButton { ToolButton {
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg" // TODO: Implement gamepad mapping then unhide this button
visible: false
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Gamepad Mapper"
Image {
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping") onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
Menu {
id: gamepadMappingMenu
x: parent.width
transformOrigin: Menu.TopRight
}
} }
ToolButton { ToolButton {
icon.source: "qrc:/res/settings.svg" Image {
source: "qrc:/res/settings.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings") onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
Menu { ToolTip.delay: 1000
id: optionsMenu ToolTip.timeout: 3000
x: parent.width ToolTip.visible: hovered
transformOrigin: Menu.TopRight ToolTip.text: "Settings"
}
} }
} }
} }
+259 -9
View File
@@ -2,6 +2,8 @@
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QIcon> #include <QIcon>
#include <QQuickStyle> #include <QQuickStyle>
#include <QMutex>
#include <QtDebug>
// Don't let SDL hook our main function, since Qt is already // Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers // doing the same thing. This needs to be before any headers
@@ -9,23 +11,265 @@
#define SDL_MAIN_HANDLED #define SDL_MAIN_HANDLED
#include <SDL.h> #include <SDL.h>
#ifdef HAVE_FFMPEG
#include "streaming/video/ffmpeg.h"
#endif
#include "path.h"
#include "gui/computermodel.h" #include "gui/computermodel.h"
#include "gui/appmodel.h" #include "gui/appmodel.h"
#include "backend/autoupdatechecker.h"
#include "streaming/session.hpp" #include "streaming/session.hpp"
#include "settings/streamingpreferences.h" #include "settings/streamingpreferences.h"
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
// Log to file for release Windows builds
#define USE_CUSTOM_LOGGER
#define LOG_TO_FILE
#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
// Use stdout logger on all Linux/BSD builds
#define USE_CUSTOM_LOGGER
#elif !defined(QT_DEBUG) && defined(Q_OS_DARWIN)
// Log to file for release Mac builds
#define USE_CUSTOM_LOGGER
#define LOG_TO_FILE
#else
// For debug Windows and Mac builds, use default logger
#endif
#ifdef USE_CUSTOM_LOGGER
static QTime s_LoggerTime;
static QTextStream s_LoggerStream(stdout);
static QMutex s_LoggerLock;
#ifdef LOG_TO_FILE
#define MAX_LOG_LINES 10000
static int s_LogLinesWritten = 0;
static bool s_LogLimitReached = false;
static QFile* s_LoggerFile;
#endif
void logToLoggerStream(QString& message)
{
QMutexLocker lock(&s_LoggerLock);
#ifdef LOG_TO_FILE
if (s_LogLimitReached) {
return;
}
else if (s_LogLinesWritten == MAX_LOG_LINES) {
s_LoggerStream << "Log size limit reached!" << endl;
s_LogLimitReached = true;
return;
}
else {
s_LogLinesWritten++;
}
#endif
s_LoggerStream << message;
s_LoggerStream.flush();
}
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
{
QString priorityTxt;
switch (priority) {
case SDL_LOG_PRIORITY_VERBOSE:
priorityTxt = "Verbose";
break;
case SDL_LOG_PRIORITY_DEBUG:
priorityTxt = "Debug";
break;
case SDL_LOG_PRIORITY_INFO:
priorityTxt = "Info";
break;
case SDL_LOG_PRIORITY_WARN:
priorityTxt = "Warn";
break;
case SDL_LOG_PRIORITY_ERROR:
priorityTxt = "Error";
break;
case SDL_LOG_PRIORITY_CRITICAL:
priorityTxt = "Critical";
break;
default:
priorityTxt = "Unknown";
break;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - SDL %2 (%3): %4\n").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
logToLoggerStream(txt);
}
void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString& msg)
{
QString typeTxt;
switch (type) {
case QtDebugMsg:
typeTxt = "Debug";
break;
case QtInfoMsg:
typeTxt = "Info";
break;
case QtWarningMsg:
typeTxt = "Warning";
break;
case QtCriticalMsg:
typeTxt = "Critical";
break;
case QtFatalMsg:
typeTxt = "Fatal";
break;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
logToLoggerStream(txt);
}
#ifdef HAVE_FFMPEG
void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
{
char lineBuffer[1024];
static int printPrefix = 1;
if ((level & 0xFF) > av_log_get_level()) {
return;
}
av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix);
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
logToLoggerStream(txt);
}
#endif
#endif
#ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <DbgHelp.h>
static UINT s_HitUnhandledException = 0;
LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
// Only write a dump for the first unhandled exception
if (InterlockedCompareExchange(&s_HitUnhandledException, 1, 0) != 0) {
return EXCEPTION_CONTINUE_SEARCH;
}
WCHAR dmpFileName[MAX_PATH];
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (dumpHandle != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION info;
info.ThreadId = GetCurrentThreadId();
info.ExceptionPointers = ExceptionInfo;
info.ClientPointers = FALSE;
DWORD typeFlags = MiniDumpWithIndirectlyReferencedMemory |
MiniDumpIgnoreInaccessibleMemory |
MiniDumpWithUnloadedModules |
MiniDumpWithThreadInfo;
if (MiniDumpWriteDump(GetCurrentProcess(),
GetCurrentProcessId(),
dumpHandle,
(MINIDUMP_TYPE)typeFlags,
&info,
nullptr,
nullptr)) {
qCritical() << "Unhandled exception! Minidump written to:" << qDmpFileName;
}
else {
qCritical() << "Unhandled exception! Failed to write dump:" << GetLastError();
}
CloseHandle(dumpHandle);
}
else {
qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError();
}
// Let the program crash and WER collect a dump
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
// Set these here to allow us to use the default QSettings constructor.
// These also ensure that our cache directory is named correctly. As such,
// it is critical that these be called before Path::initialize().
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
QCoreApplication::setApplicationName("Moonlight");
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
qInfo() << "Running in portable mode from:" << QDir::currentPath();
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QDir::currentPath());
QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, QDir::currentPath());
// Initialize paths for portable mode
Path::initialize(true);
}
else {
// Initialize paths for standard installation
Path::initialize(false);
}
#ifdef USE_CUSTOM_LOGGER
#ifdef LOG_TO_FILE
QDir tempDir(Path::getLogDir());
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
s_LoggerStream.setDevice(s_LoggerFile);
}
#endif
s_LoggerTime.start();
qInstallMessageHandler(qtLogToDiskHandler);
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
#ifdef HAVE_FFMPEG
av_log_set_callback(ffmpegLogToDiskHandler);
#endif
#endif
#ifdef Q_OS_WIN32
// Create a crash dump when we crash on Windows
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
#endif
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// This avoids using the default keychain for SSL, which may cause // This avoids using the default keychain for SSL, which may cause
// password prompts on macOS. // password prompts on macOS.
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1")); qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
// Set these here to allow us to use the default QSettings constructor #ifdef Q_OS_WIN32
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project"); // On Windows, use ANGLE so we don't have to load both DX and OGL
QCoreApplication::setOrganizationDomain("moonlight-stream.com"); // user-mode drivers into our app.
QCoreApplication::setApplicationName("Moonlight"); qputenv("QT_OPENGL", "angle");
#endif
// Register custom metatypes for use in signals // Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp"); qRegisterMetaType<NvApp>("NvApp");
@@ -44,6 +288,11 @@ int main(int argc, char *argv[])
[](QQmlEngine*, QJSEngine*) -> QObject* { [](QQmlEngine*, QJSEngine*) -> QObject* {
return new ComputerManager(); return new ComputerManager();
}); });
qmlRegisterSingletonType<AutoUpdateChecker>("AutoUpdateChecker", 1, 0,
"AutoUpdateChecker",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new AutoUpdateChecker();
});
QQuickStyle::setStyle("Material"); QQuickStyle::setStyle("Material");
@@ -53,17 +302,18 @@ int main(int argc, char *argv[])
if (engine.rootObjects().isEmpty()) if (engine.rootObjects().isEmpty())
return -1; return -1;
// Ensure that SDL is always initialized since we may need to use it
// for non-streaming purposes (like checking on audio devices)
SDL_SetMainReady(); SDL_SetMainReady();
if (SDL_Init(SDL_INIT_VIDEO | if (SDL_Init(0) != 0) {
SDL_INIT_AUDIO |
SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_Init() failed: %s", "SDL_Init() failed: %s",
SDL_GetError()); SDL_GetError());
} }
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
int err = app.exec(); int err = app.exec();
SDL_Quit(); SDL_Quit();
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

+77
View File
@@ -0,0 +1,77 @@
#include "path.h"
#include <QtDebug>
#include <QDir>
#include <QStandardPaths>
#include <QSettings>
#include <QCoreApplication>
QString Path::s_LogDir;
QString Path::s_BoxArtCacheDir;
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
QString Path::getLogDir()
{
Q_ASSERT(!s_LogDir.isEmpty());
return s_LogDir;
}
QString Path::getBoxArtCacheDir()
{
Q_ASSERT(!s_BoxArtCacheDir.isEmpty());
return s_BoxArtCacheDir;
}
QString Path::getGamepadMappingFile()
{
QString candidatePath;
// Check the current directory first
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Now check the data directories (for Linux, in particular)
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile);
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
return candidatePath;
}
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Now try the Resources folder in our app bundle for macOS
QDir dir = QDir(QCoreApplication::applicationDirPath());
dir.cdUp();
dir.cd("Resources");
dir.filePath(k_GameControllerMappingFile);
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Couldn't find it
return QString();
}
void Path::initialize(bool portable)
{
if (portable) {
s_LogDir = QDir::currentPath();
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
}
else {
#ifdef Q_OS_DARWIN
// On macOS, $TMPDIR is some random folder under /var/folders/ that nobody can
// easily find, so use the system's global tmp directory instead.
s_LogDir = "/tmp";
#else
s_LogDir = QDir::tempPath();
#endif
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
}
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <QString>
class Path
{
public:
static QString getLogDir();
static QString getBoxArtCacheDir();
static QString getGamepadMappingFile();
static void initialize(bool portable);
private:
static QString s_LogDir;
static QString s_BoxArtCacheDir;
static const QString k_GameControllerMappingFile;
};
+1
View File
@@ -6,5 +6,6 @@
<file>gui/SettingsView.qml</file> <file>gui/SettingsView.qml</file>
<file>gui/StreamSegue.qml</file> <file>gui/StreamSegue.qml</file>
<file>gui/GamepadMapper.qml</file> <file>gui/GamepadMapper.qml</file>
<file>gui/QuitSegue.qml</file>
</qresource> </qresource>
</RCC> </RCC>
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="#fff">
<path d="M0 0h48v48H0z" fill="none"/>
<path d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm-4 29V15l12 9-12 9z"/>
</svg>

After

Width:  |  Height:  |  Size: 250 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.
+67 -21
View File
@@ -1,23 +1,69 @@
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.0"> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<defs> <svg
<filter id="f1" y="-40%" width="120%" height="220%"> xmlns:dc="http://purl.org/dc/elements/1.1/"
<feOffset xmlns="http://www.w3.org/2000/svg" result="offOut" in="SourceAlpha" dy="-100"/> xmlns:cc="http://creativecommons.org/ns#"
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="200"/> xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
<feBlend in="SourceGraphic" in2="blurOut" mode="normal"/> xmlns:svg="http://www.w3.org/2000/svg"
</filter> xmlns="http://www.w3.org/2000/svg"
</defs> id="svg3761"
<!-- -105, 521 are within 0.5 pixel-perfect --> viewBox="0 0 680 680"
<!-- offsetting more to show shadow --> height="680"
<g transform="translate(-95,530) scale(0.100000,-0.100000)" filter="url(#f1)"> width="680"
<circle cx="4755" cy="1505" r="3700" fill="#565C64"> version="1.0">
</circle> <metadata
<path d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" style="fill:#ffffff"/> id="metadata3767">
<path xmlns="http://www.w3.org/2000/svg" d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" style="fill:#ffffff"/> <rdf:RDF>
<path d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" style="fill:#ffffff"/> <cc:Work
<path d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" style="fill:#ffffff"/> rdf:about="">
<path d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" style="fill:#ffffff"/> <dc:format>image/svg+xml</dc:format>
<path d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" style="fill:#ffffff"/> <dc:type
<path d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" style="fill:#ffffff"/> rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<path d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" style="fill:#ffffff"/> <dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3765" />
<g
id="g3759"
transform="translate(-135,491) scale(0.100000,-0.100000)">
<circle
id="circle3741"
fill="#565C64"
r="3400"
cy="1505"
cx="4755" />
<path
id="path3743"
style="fill:#ffffff"
d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" />
<path
id="path3745"
style="fill:#ffffff"
d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" />
<path
id="path3747"
style="fill:#ffffff"
d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" />
<path
id="path3749"
style="fill:#ffffff"
d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" />
<path
id="path3751"
style="fill:#ffffff"
d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" />
<path
id="path3753"
style="fill:#ffffff"
d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" />
<path
id="path3755"
style="fill:#ffffff"
d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" />
<path
id="path3757"
style="fill:#ffffff"
d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200">
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
</svg>

After

Width:  |  Height:  |  Size: 499 B

+4
View File
@@ -5,10 +5,14 @@
<file>res/ic_videogame_asset_white_48px.svg</file> <file>res/ic_videogame_asset_white_48px.svg</file>
<file>res/ic_tv_white_48px.svg</file> <file>res/ic_tv_white_48px.svg</file>
<file>res/ic_add_to_queue_white_48px.svg</file> <file>res/ic_add_to_queue_white_48px.svg</file>
<file>res/baseline-lock-24px.svg</file>
<file>res/baseline-play_circle_filled_white-48px.svg</file>
<file>res/baseline-warning-24px.svg</file>
<file>res/no_app_image.png</file> <file>res/no_app_image.png</file>
<file>res/settings.svg</file> <file>res/settings.svg</file>
<file>res/arrow_left.svg</file> <file>res/arrow_left.svg</file>
<file>res/question_mark.svg</file> <file>res/question_mark.svg</file>
<file>res/moonlight.svg</file> <file>res/moonlight.svg</file>
<file>res/update.svg</file>
</qresource> </qresource>
</RCC> </RCC>
+143 -16
View File
@@ -1,4 +1,6 @@
#include "streamingpreferences.h" #include "streamingpreferences.h"
#include "streaming/session.hpp"
#include "streaming/streamutils.h"
#include <QSettings> #include <QSettings>
@@ -8,12 +10,17 @@
#define SER_FPS "fps" #define SER_FPS "fps"
#define SER_BITRATE "bitrate" #define SER_BITRATE "bitrate"
#define SER_FULLSCREEN "fullscreen" #define SER_FULLSCREEN "fullscreen"
#define SER_VSYNC "vsync"
#define SER_GAMEOPTS "gameopts" #define SER_GAMEOPTS "gameopts"
#define SER_HOSTAUDIO "hostaudio" #define SER_HOSTAUDIO "hostaudio"
#define SER_MULTICONT "multicontroller" #define SER_MULTICONT "multicontroller"
#define SER_AUDIOCFG "audiocfg" #define SER_AUDIOCFG "audiocfg"
#define SER_VIDEOCFG "videocfg" #define SER_VIDEOCFG "videocfg"
#define SER_VIDEODEC "videodec" #define SER_VIDEODEC "videodec"
#define SER_WINDOWMODE "windowmode"
#define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns"
#define SER_MOUSEACCELERATION "mouseacceleration"
StreamingPreferences::StreamingPreferences() StreamingPreferences::StreamingPreferences()
{ {
@@ -28,16 +35,23 @@ void StreamingPreferences::reload()
height = settings.value(SER_HEIGHT, 720).toInt(); height = settings.value(SER_HEIGHT, 720).toInt();
fps = settings.value(SER_FPS, 60).toInt(); fps = settings.value(SER_FPS, 60).toInt();
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt(); bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
fullScreen = settings.value(SER_FULLSCREEN, true).toBool(); enableVsync = settings.value(SER_VSYNC, true).toBool();
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool(); gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool(); playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
multiController = settings.value(SER_MULTICONT, true).toBool(); multiController = settings.value(SER_MULTICONT, true).toBool();
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool();
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG, audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_AUTO)).toInt()); static_cast<int>(AudioConfig::AC_AUTO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG, videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt()); static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC, videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt()); static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
// Try to load from the old preference value too
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
} }
void StreamingPreferences::save() void StreamingPreferences::save()
@@ -48,33 +62,146 @@ void StreamingPreferences::save()
settings.setValue(SER_HEIGHT, height); settings.setValue(SER_HEIGHT, height);
settings.setValue(SER_FPS, fps); settings.setValue(SER_FPS, fps);
settings.setValue(SER_BITRATE, bitrateKbps); settings.setValue(SER_BITRATE, bitrateKbps);
settings.setValue(SER_FULLSCREEN, fullScreen); settings.setValue(SER_VSYNC, enableVsync);
settings.setValue(SER_GAMEOPTS, gameOptimizations); settings.setValue(SER_GAMEOPTS, gameOptimizations);
settings.setValue(SER_HOSTAUDIO, playAudioOnHost); settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
settings.setValue(SER_MULTICONT, multiController); settings.setValue(SER_MULTICONT, multiController);
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig)); settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig)); settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection)); settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
}
bool StreamingPreferences::hasAnyHardwareAcceleration()
{
// Always use VDS_AUTO to avoid spamming the user with warnings
// if they've forced software decoding.
return Session::isHardwareDecodeAvailable(VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
}
bool StreamingPreferences::isRunningWayland()
{
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
}
int StreamingPreferences::getMaximumStreamingFrameRate()
{
// Never let the maximum drop below 60 FPS
int maxFrameRate = 60;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return maxFrameRate;
}
SDL_DisplayMode mode, bestMode, desktopMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
// Get the native desktop resolution
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
// Start at desktop mode and work our way up
bestMode = desktopMode;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
// Cap the frame rate at 120 FPS. Past this, the encoders start
// to max out and drop frames.
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
}
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return maxFrameRate;
}
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (err == 0) {
return QRect(0, 0, mode.w, mode.h);
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return QRect();
}
}
QRect StreamingPreferences::getNativeResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (success) {
return QRect(0, 0, mode.w, mode.h);
}
else {
return QRect();
}
} }
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps) int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
{ {
if (width * height * fps <= 1280 * 720 * 30) { // This table prefers 16:10 resolutions because they are
return 5000; // only slightly more pixels than the 16:9 equivalents, so
// we don't want to bump those 16:10 resolutions up to the
// next 16:9 slot.
// This covers 1280x720 and 1280x800 too
if (width * height <= 1366 * 768) {
return static_cast<int>(5000 * (fps / 30.0));
} }
else if (width * height * fps <= 1280 * 720 * 60) { else if (width * height <= 1920 * 1200) {
return 10000; return static_cast<int>(10000 * (fps / 30.0));
} }
else if (width * height * fps <= 1920 * 1080 * 30) { else if (width * height <= 2560 * 1600) {
return 10000; return static_cast<int>(20000 * (fps / 30.0));
} }
else if (width * height * fps <= 1920 * 1080 * 60) { else /* if (width * height <= 3840 * 2160) */ {
return 20000; return static_cast<int>(40000 * (fps / 30.0));
}
else if (width * height * fps <= 3840 * 2160 * 30) {
return 40000;
}
else /* if (width * height * fps <= 3840 * 2160 * 60) */ {
return 80000;
} }
} }
+34 -3
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QRect>
class StreamingPreferences : public QObject class StreamingPreferences : public QObject
{ {
@@ -14,6 +15,16 @@ public:
Q_INVOKABLE void save(); Q_INVOKABLE void save();
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
Q_INVOKABLE static bool isRunningWayland();
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
void reload(); void reload();
enum AudioConfig enum AudioConfig
@@ -41,40 +52,60 @@ public:
}; };
Q_ENUM(VideoDecoderSelection) Q_ENUM(VideoDecoderSelection)
enum WindowMode
{
WM_FULLSCREEN,
WM_FULLSCREEN_DESKTOP,
WM_WINDOWED
};
Q_ENUM(WindowMode)
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged) Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged) Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged) Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged) Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
Q_PROPERTY(bool fullScreen MEMBER fullScreen NOTIFY fullScreenChanged) Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged) Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged) Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged) Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged) Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged) Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged) Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
// Directly accessible members for preferences // Directly accessible members for preferences
int width; int width;
int height; int height;
int fps; int fps;
int bitrateKbps; int bitrateKbps;
bool fullScreen; bool enableVsync;
bool gameOptimizations; bool gameOptimizations;
bool playAudioOnHost; bool playAudioOnHost;
bool multiController; bool multiController;
bool unsupportedFps;
bool enableMdns;
bool mouseAcceleration;
AudioConfig audioConfig; AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig; VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection; VideoDecoderSelection videoDecoderSelection;
WindowMode windowMode;
signals: signals:
void displayModeChanged(); void displayModeChanged();
void bitrateChanged(); void bitrateChanged();
void fullScreenChanged(); void enableVsyncChanged();
void gameOptimizationsChanged(); void gameOptimizationsChanged();
void playAudioOnHostChanged(); void playAudioOnHostChanged();
void multiControllerChanged(); void multiControllerChanged();
void unsupportedFpsChanged();
void enableMdnsChanged();
void mouseAccelerationChanged();
void audioConfigChanged(); void audioConfigChanged();
void videoCodecConfigChanged(); void videoCodecConfigChanged();
void videoDecoderSelectionChanged(); void videoDecoderSelectionChanged();
void windowModeChanged();
}; };
-198
View File
@@ -1,198 +0,0 @@
#include "session.hpp"
#include <Limelight.h>
#include <SDL.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
#define MIN_QUEUED_FRAMES 2
#define MAX_QUEUED_FRAMES 4
#define STOP_THE_WORLD_LIMIT 20
#define DROP_RATIO_DENOM 32
SDL_AudioDeviceID Session::s_AudioDevice;
OpusMSDecoder* Session::s_OpusDecoder;
short Session::s_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
int Session::s_ChannelCount;
int Session::s_PendingDrops;
int Session::s_PendingHardDrops;
unsigned int Session::s_SampleIndex;
Uint32 Session::s_BaselinePendingData;
int Session::sdlDetermineAudioConfiguration()
{
SDL_AudioSpec want, have;
SDL_AudioDeviceID dev;
SDL_zero(want);
want.freq = 48000;
want.format = AUDIO_S16;
want.channels = 6;
want.samples = 1024;
// Try to open for 5.1 surround sound, but allow SDL to tell us that's
// not available.
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
if (dev == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device");
// We'll probably have issues during audio stream init, but we'll
// try anyway
return AUDIO_CONFIGURATION_STEREO;
}
SDL_CloseAudioDevice(dev);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio device has %d channels", have.channels);
if (have.channels > 2) {
// We don't support quadraphonic or 7.1 surround, but SDL
// should be able to downmix or upmix better from 5.1 than
// from stereo, so use 5.1 in non-stereo cases.
return AUDIO_CONFIGURATION_51_SURROUND;
}
else {
return AUDIO_CONFIGURATION_STEREO;
}
}
int Session::sdlAudioInit(int /* audioConfiguration */,
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* /* arContext */, int /* arFlags */)
{
SDL_AudioSpec want, have;
int error;
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
want.channels = opusConfig->channelCount;
// This is supposed to be a power of 2, but our
// frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms.
want.samples = SAMPLES_PER_FRAME;
s_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (s_AudioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s",
SDL_GetError());
return -1;
}
s_OpusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
&error);
if (s_OpusDecoder == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create decoder: %d",
error);
SDL_CloseAudioDevice(s_AudioDevice);
s_AudioDevice = 0;
return -2;
}
// SDL counts pending samples in the queued
// audio size using the WASAPI backend. This
// includes silence, which can throw off our
// pending data count. Get a baseline so we
// can exclude that data.
s_BaselinePendingData = 0;
#ifdef _WIN32
for (int i = 0; i < 100; i++) {
s_BaselinePendingData = qMax(s_BaselinePendingData, SDL_GetQueuedAudioSize(s_AudioDevice));
SDL_Delay(10);
}
#endif
s_BaselinePendingData *= 2;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Baseline pending audio data: %d bytes",
s_BaselinePendingData);
s_ChannelCount = opusConfig->channelCount;
s_SampleIndex = 0;
s_PendingDrops = s_PendingHardDrops = 0;
return 0;
}
void Session::sdlAudioStart()
{
// Unpause the audio device
SDL_PauseAudioDevice(s_AudioDevice, 0);
}
void Session::sdlAudioStop()
{
// Pause the audio device
SDL_PauseAudioDevice(s_AudioDevice, 1);
}
void Session::sdlAudioCleanup()
{
SDL_CloseAudioDevice(s_AudioDevice);
s_AudioDevice = 0;
opus_multistream_decoder_destroy(s_OpusDecoder);
s_OpusDecoder = NULL;
}
void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int samplesDecoded;
s_SampleIndex++;
Uint32 queuedAudio = qMax((int)SDL_GetQueuedAudioSize(s_AudioDevice) - (int)s_BaselinePendingData, 0);
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * s_ChannelCount * sizeof(short));
// Pend enough drops to get us back to MIN_QUEUED_FRAMES
if (framesQueued - s_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
s_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Pending hard drop of %u audio frames",
s_PendingHardDrops);
}
else if (framesQueued - s_PendingHardDrops - s_PendingDrops > MAX_QUEUED_FRAMES) {
s_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
}
// Determine if this frame should be dropped
if (framesQueued <= MIN_QUEUED_FRAMES) {
s_PendingDrops = s_PendingHardDrops = 0;
}
else if (s_PendingHardDrops != 0) {
// Hard drops happen all at once to forcefully
// resync with the source.
s_PendingHardDrops--;
return;
}
else if (s_PendingDrops != 0 && s_SampleIndex % DROP_RATIO_DENOM == 0) {
// Normal drops are interspersed with the audio data
// to hide the glitches.
s_PendingDrops--;
return;
}
samplesDecoded = opus_multistream_decode(s_OpusDecoder,
(unsigned char*)sampleData,
sampleLength,
s_OpusDecodeBuffer,
SAMPLES_PER_FRAME,
0);
if (samplesDecoded > 0) {
if (SDL_QueueAudio(s_AudioDevice,
s_OpusDecodeBuffer,
sizeof(short) * samplesDecoded * s_ChannelCount) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s",
SDL_GetError());
}
}
}
+124
View File
@@ -0,0 +1,124 @@
#include "../session.hpp"
#include "renderers/renderer.h"
#include "renderers/sdl.h"
#include "renderers/qtaud.h"
#include <Limelight.h>
IAudioRenderer* Session::createAudioRenderer()
{
return new SdlAudioRenderer();
}
bool Session::testAudio(int audioConfiguration)
{
IAudioRenderer* audioRenderer;
audioRenderer = createAudioRenderer();
if (audioRenderer == nullptr) {
return false;
}
bool ret = audioRenderer->testAudio(audioConfiguration);
delete audioRenderer;
return ret;
}
int Session::detectAudioConfiguration()
{
IAudioRenderer* audioRenderer;
audioRenderer = createAudioRenderer();
if (audioRenderer == nullptr) {
// Hope for the best
return AUDIO_CONFIGURATION_STEREO;
}
int audioConfig = audioRenderer->detectAudioConfiguration();
delete audioRenderer;
return audioConfig;
}
int Session::arInit(int /* audioConfiguration */,
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* /* arContext */, int /* arFlags */)
{
int error;
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
s_ActiveSession->m_OpusDecoder =
opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
&error);
if (s_ActiveSession->m_OpusDecoder == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create decoder: %d",
error);
return -1;
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (s_ActiveSession->m_AudioRenderer == nullptr) {
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -2;
}
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
delete s_ActiveSession->m_AudioRenderer;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -3;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
opusConfig->channelCount);
return 0;
}
void Session::arCleanup()
{
// m_AudioRenderer is deleted in cleanupAudioRenderer()
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
s_ActiveSession->m_OpusDecoder = nullptr;
}
// This is called on the main thread
void Session::cleanupAudioRendererOnMainThread()
{
SDL_AtomicLock(&m_AudioRendererLock);
delete m_AudioRenderer;
m_AudioRenderer = nullptr;
SDL_AtomicUnlock(&m_AudioRendererLock);
}
void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int samplesDecoded;
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
(unsigned char*)sampleData,
sampleLength,
s_ActiveSession->m_OpusDecodeBuffer,
SAMPLES_PER_FRAME,
0);
if (samplesDecoded > 0) {
SDL_AtomicLock(&s_ActiveSession->m_AudioRendererLock);
if (s_ActiveSession->m_AudioRenderer != nullptr) {
s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
static_cast<int>(sizeof(short) *
samplesDecoded *
s_ActiveSession->m_AudioConfig.channelCount));
}
SDL_AtomicUnlock(&s_ActiveSession->m_AudioRendererLock);
}
}
+96
View File
@@ -0,0 +1,96 @@
#include "qtaud.h"
#include <QDebug>
QtAudioRenderer::QtAudioRenderer()
: m_AudioOutput(nullptr),
m_OutputDevice(nullptr)
{
}
QtAudioRenderer::~QtAudioRenderer()
{
delete m_AudioOutput;
}
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
QAudioFormat format;
// testAudio() assumes this sample rate
Q_ASSERT(opusConfig->sampleRate == 48000);
format.setSampleRate(opusConfig->sampleRate);
format.setChannelCount(opusConfig->channelCount);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setCodec("audio/pcm");
m_AudioOutput = new QAudioOutput(format);
// Buffer 40 ms of audio data
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
m_OutputDevice = m_AudioOutput->start();
if (m_OutputDevice == nullptr) {
qWarning() << "Audio start failed:" << m_AudioOutput->error();
return false;
}
// Check what buffer size was really used after starting
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
return true;
}
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{
m_OutputDevice->write((const char *)audioBuffer, audioSize);
}
bool QtAudioRenderer::testAudio(int audioConfiguration)
{
QAudioFormat format;
format.setSampleRate(48000);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setCodec("audio/pcm");
switch (audioConfiguration) {
case AUDIO_CONFIGURATION_STEREO:
format.setChannelCount(2);
break;
case AUDIO_CONFIGURATION_51_SURROUND:
format.setChannelCount(6);
break;
default:
Q_ASSERT(false);
return false;
}
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
return true;
}
else {
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
return false;
}
}
int QtAudioRenderer::detectAudioConfiguration()
{
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
// We can better downmix 5.1 to quad than we can upmix stereo
if (preferredChannelCount > 2) {
return AUDIO_CONFIGURATION_51_SURROUND;
}
else {
return AUDIO_CONFIGURATION_STEREO;
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "renderer.h"
#include <QAudioOutput>
class QtAudioRenderer : public IAudioRenderer
{
public:
QtAudioRenderer();
virtual ~QtAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void submitAudio(short* audioBuffer, int audioSize);
virtual bool testAudio(int audioConfiguration);
virtual int detectAudioConfiguration();
private:
QAudioOutput* m_AudioOutput;
QIODevice* m_OutputDevice;
};
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <Limelight.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
class IAudioRenderer
{
public:
virtual ~IAudioRenderer() {}
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
virtual void submitAudio(short* audioBuffer, int audioSize) = 0;
virtual bool testAudio(int audioConfiguration) = 0;
virtual int detectAudioConfiguration() = 0;
};
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "renderer.h"
#include <SDL.h>
class SdlAudioRenderer : public IAudioRenderer
{
public:
SdlAudioRenderer();
virtual ~SdlAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void submitAudio(short* audioBuffer, int audioSize);
virtual bool testAudio(int audioConfiguration);
virtual int detectAudioConfiguration();
private:
SDL_AudioDeviceID m_AudioDevice;
Uint32 m_ChannelCount;
Uint32 m_PendingDrops;
Uint32 m_PendingHardDrops;
Uint32 m_SampleIndex;
Uint32 m_BaselinePendingData;
};
+218
View File
@@ -0,0 +1,218 @@
#include "sdl.h"
#include <Limelight.h>
#include <SDL.h>
#include <QAudioDeviceInfo>
#include <QtGlobal>
#define MIN_QUEUED_FRAMES 2
#define MAX_QUEUED_FRAMES 4
#define STOP_THE_WORLD_LIMIT 20
#define DROP_RATIO_DENOM 32
// Detecting this with SDL is quite problematic, so we'll use Qt's
// multimedia framework to do so. It appears to be actually
// accurate on Linux and macOS, unlike using SDL and relying
// on a channel change in the format received.
int SdlAudioRenderer::detectAudioConfiguration()
{
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio output device prefers %d channel configuration",
preferredChannelCount);
// We can better downmix 5.1 to quad than we can upmix stereo
if (preferredChannelCount > 2) {
return AUDIO_CONFIGURATION_51_SURROUND;
}
else {
return AUDIO_CONFIGURATION_STEREO;
}
}
bool SdlAudioRenderer::testAudio(int audioConfiguration)
{
SDL_AudioSpec want, have;
SDL_AudioDeviceID dev;
SDL_zero(want);
want.freq = 48000;
want.format = AUDIO_S16;
want.samples = SAMPLES_PER_FRAME;
switch (audioConfiguration) {
case AUDIO_CONFIGURATION_STEREO:
want.channels = 2;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
want.channels = 6;
break;
default:
SDL_assert(false);
return false;
}
// Test audio device for functionality
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (dev == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Audio test - Failed to open audio device: %s",
SDL_GetError());
return false;
}
SDL_CloseAudioDevice(dev);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio test - Successful with %d channels",
want.channels);
return true;
}
SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0),
m_ChannelCount(0),
m_PendingDrops(0),
m_PendingHardDrops(0),
m_SampleIndex(0),
m_BaselinePendingData(0)
{
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
SDL_assert(SDL_WasInit(SDL_INIT_AUDIO));
}
}
bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
SDL_AudioSpec want, have;
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
want.channels = opusConfig->channelCount;
// This is supposed to be a power of 2, but our
// frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms.
want.samples = SAMPLES_PER_FRAME;
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (m_AudioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Desired audio buffer: %u samples (%lu bytes)",
want.samples,
want.samples * sizeof(short) * want.channels);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Obtained audio buffer: %u samples (%u bytes)",
have.samples,
have.size);
// SDL counts pending samples in the queued
// audio size using the WASAPI backend. This
// includes silence, which can throw off our
// pending data count. Get a baseline so we
// can exclude that data.
m_BaselinePendingData = 0;
#ifdef Q_OS_WIN32
for (int i = 0; i < 100; i++) {
m_BaselinePendingData = qMax(m_BaselinePendingData, SDL_GetQueuedAudioSize(m_AudioDevice));
SDL_Delay(10);
}
#endif
m_BaselinePendingData *= 2;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Baseline pending audio data: %d bytes",
m_BaselinePendingData);
m_ChannelCount = opusConfig->channelCount;
m_SampleIndex = 0;
m_PendingDrops = m_PendingHardDrops = 0;
// Start playback
SDL_PauseAudioDevice(m_AudioDevice, 0);
return true;
}
SdlAudioRenderer::~SdlAudioRenderer()
{
if (m_AudioDevice != 0) {
// Stop playback
SDL_PauseAudioDevice(m_AudioDevice, 1);
SDL_CloseAudioDevice(m_AudioDevice);
}
SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
}
void SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{
m_SampleIndex++;
Uint32 queuedAudio = SDL_GetQueuedAudioSize(m_AudioDevice);
if (queuedAudio > m_BaselinePendingData) {
queuedAudio -= m_BaselinePendingData;
}
else {
queuedAudio = 0;
}
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * m_ChannelCount * sizeof(short));
// We must check this prior to the below checks to ensure we don't
// underflow if framesQueued - m_PendingHardDrops < 0.
if (framesQueued <= MIN_QUEUED_FRAMES) {
m_PendingDrops = m_PendingHardDrops = 0;
}
// Pend enough drops to get us back to MIN_QUEUED_FRAMES, checking first
// to ensure we don't underflow.
else if (framesQueued > m_PendingHardDrops &&
framesQueued - m_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
m_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Pending hard drop of %u audio frames",
m_PendingHardDrops);
}
// If we're under the stop the world limit, we can drop samples
// gracefully over the next little while.
else if (framesQueued > m_PendingHardDrops + m_PendingDrops &&
framesQueued - m_PendingHardDrops - m_PendingDrops > MAX_QUEUED_FRAMES) {
m_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
}
// Determine if this frame should be dropped
if (m_PendingHardDrops != 0) {
// Hard drops happen all at once to forcefully
// resync with the source.
m_PendingHardDrops--;
return;
}
else if (m_PendingDrops != 0 && m_SampleIndex % DROP_RATIO_DENOM == 0) {
// Normal drops are interspersed with the audio data
// to hide the glitches.
m_PendingDrops--;
return;
}
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s",
SDL_GetError());
}
}
+213 -99
View File
@@ -1,6 +1,10 @@
#include <Limelight.h> #include <Limelight.h>
#include <SDL.h> #include <SDL.h>
#include "streaming/session.hpp" #include "streaming/session.hpp"
#include "path.h"
#include <QtGlobal>
#include <QDir>
#define VK_0 0x30 #define VK_0 0x30
#define VK_A 0x41 #define VK_A 0x41
@@ -9,7 +13,7 @@
#ifndef VK_F1 #ifndef VK_F1
#define VK_F1 0x70 #define VK_F1 0x70
#define VK_F13 0x7C #define VK_F13 0x7C
#define VK_NUMPAD1 0x61 #define VK_NUMPAD0 0x60
#endif #endif
const int SdlInputHandler::k_ButtonMap[] = { const int SdlInputHandler::k_ButtonMap[] = {
@@ -20,13 +24,76 @@ const int SdlInputHandler::k_ButtonMap[] = {
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
}; };
SdlInputHandler::SdlInputHandler(bool multiController) SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer)
: m_MultiController(multiController) : m_LastMouseMotionTime(0),
m_MultiController(prefs.multiController),
m_NeedsInputDelay(false)
{ {
// Allow gamepad input when the app doesn't have focus
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// If mouse acceleration is enabled, use relative mode warp (which
// is via normal motion events that are influenced by mouse acceleration).
// Otherwise, we'll use raw input capture which is straight from the device
// without modification by the OS.
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.mouseAcceleration ? "1" : "0");
// We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
QString mappingFile = Path::getGamepadMappingFile();
if (!mappingFile.isEmpty()) {
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loading gamepad mappings from: %s",
mappingFileNative.c_str());
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
if (newMappings < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading gamepad mappings: %s",
SDL_GetError());
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loaded %d new gamepad mappings",
newMappings);
}
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No gamepad mapping file found");
}
if (!m_MultiController) { if (!m_MultiController) {
// Player 1 is always present in non-MC mode // Player 1 is always present in non-MC mode
m_GamepadMask = 0x1; m_GamepadMask = 0x1;
} }
else {
// Otherwise, detect gamepads on the fly
m_GamepadMask = 0;
}
// Prior to GFE 3.14.1, sending too many mouse motion events can cause
// GFE to choke and input latency to increase significantly. We will
// artificially throttle them to avoid this situation.
QVector<int> gfeVersion = NvHTTP::parseQuad(computer->gfeVersion);
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
gfeVersion[0] < 3 ||
(gfeVersion[0] == 3 && gfeVersion[1] < 14) ||
(gfeVersion[0] == 3 && gfeVersion[1] == 14 && gfeVersion[2] < 1)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"This older version of GFE requires input delay hack");
m_NeedsInputDelay = true;
}
SDL_zero(m_GamepadState); SDL_zero(m_GamepadState);
} }
@@ -38,6 +105,9 @@ SdlInputHandler::~SdlInputHandler()
SDL_GameControllerClose(m_GamepadState[i].controller); SDL_GameControllerClose(m_GamepadState[i].controller);
} }
} }
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
} }
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
@@ -51,26 +121,26 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
(event->keysym.mod & KMOD_ALT) && (event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) { (event->keysym.mod & KMOD_SHIFT)) {
// Force raise all keys in the combo to avoid
// leaving them down after disconnecting
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
// Check for quit combo (Ctrl+Alt+Shift+Q) // Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.sym == SDLK_q) { if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo"); "Detected quit key combo");
// Force raise all keys in the combo to avoid // Uncapture the mouse to avoid processing any
// leaving them down after disconnecting // further keyboard input
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0); SDL_SetRelativeMouseMode(SDL_FALSE);
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
SDL_Event event; SDL_Event event;
// Drain the event queue of any additional input
// that might be processed before our quit message.
while (SDL_PollEvent(&event));
// Push a quit event to the main loop // Push a quit event to the main loop
event.type = SDL_QUIT; event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks(); event.quit.timestamp = SDL_GetTicks();
@@ -88,12 +158,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
else if (event->keysym.sym == SDLK_x) { else if (event->keysym.sym == SDLK_x) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo"); "Detected full-screen toggle combo");
if (SDL_GetWindowFlags(Session::s_ActiveSession->m_Window) & SDL_WINDOW_FULLSCREEN) { Session::s_ActiveSession->toggleFullscreen();
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, 0);
}
else {
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, SDL_WINDOW_FULLSCREEN);
}
return; return;
} }
} }
@@ -115,195 +180,206 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
modifiers |= MODIFIER_SHIFT; modifiers |= MODIFIER_SHIFT;
} }
// Set keycode // Set keycode. We explicitly use scancode here because GFE will try to correct
if (event->keysym.sym >= SDLK_0 && event->keysym.sym <= SDLK_9) { // for AZERTY layouts on the host but it depends on receiving VK_ values matching
keyCode = (event->keysym.sym - SDLK_0) + VK_0; // a QWERTY layout to work.
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
} }
else if (event->keysym.sym >= SDLK_a && event->keysym.sym <= SDLK_z) { else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
keyCode = (event->keysym.sym - SDLK_a) + VK_A; keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
} }
else if (event->keysym.sym >= SDLK_F1 && event->keysym.sym <= SDLK_F12) { else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
keyCode = (event->keysym.sym - SDLK_F1) + VK_F1; keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
} }
else if (event->keysym.sym >= SDLK_F13 && event->keysym.sym <= SDLK_F24) { else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
keyCode = (event->keysym.sym - SDLK_F13) + VK_F13; keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
} }
else if (event->keysym.sym >= SDLK_KP_1 && event->keysym.sym <= SDLK_KP_9) { else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
// SDL defines SDLK_KP_0 > SDLK_KP_9, so we need to handle that manually // SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
keyCode = (event->keysym.sym - SDLK_KP_1) + VK_NUMPAD1; keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
} }
else { else {
switch (event->keysym.sym) { switch (event->keysym.scancode) {
case SDLK_BACKSPACE: case SDL_SCANCODE_BACKSPACE:
keyCode = 0x08; keyCode = 0x08;
break; break;
case SDLK_TAB: case SDL_SCANCODE_TAB:
keyCode = 0x09; keyCode = 0x09;
break; break;
case SDLK_CLEAR: case SDL_SCANCODE_CLEAR:
keyCode = 0x0C; keyCode = 0x0C;
break; break;
case SDLK_RETURN: case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct?
case SDL_SCANCODE_RETURN:
keyCode = 0x0D; keyCode = 0x0D;
break; break;
case SDLK_PAUSE: case SDL_SCANCODE_PAUSE:
keyCode = 0x13; keyCode = 0x13;
break; break;
case SDLK_CAPSLOCK: case SDL_SCANCODE_CAPSLOCK:
keyCode = 0x14; keyCode = 0x14;
break; break;
case SDLK_ESCAPE: case SDL_SCANCODE_ESCAPE:
keyCode = 0x1B; keyCode = 0x1B;
break; break;
case SDLK_SPACE: case SDL_SCANCODE_SPACE:
keyCode = 0x20; keyCode = 0x20;
break; break;
case SDLK_PAGEUP: case SDL_SCANCODE_PAGEUP:
keyCode = 0x21; keyCode = 0x21;
break; break;
case SDLK_PAGEDOWN: case SDL_SCANCODE_PAGEDOWN:
keyCode = 0x22; keyCode = 0x22;
break; break;
case SDLK_END: case SDL_SCANCODE_END:
keyCode = 0x23; keyCode = 0x23;
break; break;
case SDLK_HOME: case SDL_SCANCODE_HOME:
keyCode = 0x24; keyCode = 0x24;
break; break;
case SDLK_LEFT: case SDL_SCANCODE_LEFT:
keyCode = 0x25; keyCode = 0x25;
break; break;
case SDLK_UP: case SDL_SCANCODE_UP:
keyCode = 0x26; keyCode = 0x26;
break; break;
case SDLK_RIGHT: case SDL_SCANCODE_RIGHT:
keyCode = 0x27; keyCode = 0x27;
break; break;
case SDLK_DOWN: case SDL_SCANCODE_DOWN:
keyCode = 0x28; keyCode = 0x28;
break; break;
case SDLK_SELECT: case SDL_SCANCODE_SELECT:
keyCode = 0x29; keyCode = 0x29;
break; break;
case SDLK_EXECUTE: case SDL_SCANCODE_EXECUTE:
keyCode = 0x2B; keyCode = 0x2B;
break; break;
case SDLK_PRINTSCREEN: case SDL_SCANCODE_PRINTSCREEN:
keyCode = 0x2C; keyCode = 0x2C;
break; break;
case SDLK_INSERT: case SDL_SCANCODE_INSERT:
keyCode = 0x2D; keyCode = 0x2D;
break; break;
case SDLK_DELETE: case SDL_SCANCODE_DELETE:
keyCode = 0x2E; keyCode = 0x2E;
break; break;
case SDLK_HELP: case SDL_SCANCODE_HELP:
keyCode = 0x2F; keyCode = 0x2F;
break; break;
case SDLK_KP_0: case SDL_SCANCODE_KP_0:
// See comment above about why we only handle KP_0 here // See comment above about why we only handle SDL_SCANCODE_KP_0 here
keyCode = 0x60; keyCode = VK_NUMPAD0;
break; break;
case SDLK_KP_MULTIPLY: case SDL_SCANCODE_0:
// See comment above about why we only handle SDL_SCANCODE_0 here
keyCode = VK_0;
break;
case SDL_SCANCODE_KP_MULTIPLY:
keyCode = 0x6A; keyCode = 0x6A;
break; break;
case SDLK_KP_PLUS: case SDL_SCANCODE_KP_PLUS:
keyCode = 0x6B; keyCode = 0x6B;
break; break;
case SDLK_KP_COMMA: case SDL_SCANCODE_KP_COMMA:
keyCode = 0x6C; keyCode = 0x6C;
break; break;
case SDLK_KP_MINUS: case SDL_SCANCODE_KP_MINUS:
keyCode = 0x6D; keyCode = 0x6D;
break; break;
case SDLK_KP_DECIMAL: case SDL_SCANCODE_KP_PERIOD:
keyCode = 0x6E; keyCode = 0x6E;
break; break;
case SDLK_KP_DIVIDE: case SDL_SCANCODE_KP_DIVIDE:
keyCode = 0x6F; keyCode = 0x6F;
break; break;
case SDLK_NUMLOCKCLEAR: case SDL_SCANCODE_NUMLOCKCLEAR:
keyCode = 0x90; keyCode = 0x90;
break; break;
case SDLK_SCROLLLOCK: case SDL_SCANCODE_SCROLLLOCK:
keyCode = 0x91; keyCode = 0x91;
break; break;
case SDLK_LSHIFT: case SDL_SCANCODE_LSHIFT:
keyCode = 0xA0; keyCode = 0xA0;
break; break;
case SDLK_RSHIFT: case SDL_SCANCODE_RSHIFT:
keyCode = 0xA1; keyCode = 0xA1;
break; break;
case SDLK_LCTRL: case SDL_SCANCODE_LCTRL:
keyCode = 0xA2; keyCode = 0xA2;
break; break;
case SDLK_RCTRL: case SDL_SCANCODE_RCTRL:
keyCode = 0xA3; keyCode = 0xA3;
break; break;
case SDLK_LALT: case SDL_SCANCODE_LALT:
keyCode = 0xA4; keyCode = 0xA4;
break; break;
case SDLK_RALT: case SDL_SCANCODE_RALT:
keyCode = 0xA5; keyCode = 0xA5;
break; break;
case SDLK_AC_BACK: case SDL_SCANCODE_AC_BACK:
keyCode = 0xA6; keyCode = 0xA6;
break; break;
case SDLK_AC_FORWARD: case SDL_SCANCODE_AC_FORWARD:
keyCode = 0xA7; keyCode = 0xA7;
break; break;
case SDLK_AC_REFRESH: case SDL_SCANCODE_AC_REFRESH:
keyCode = 0xA8; keyCode = 0xA8;
break; break;
case SDLK_AC_STOP: case SDL_SCANCODE_AC_STOP:
keyCode = 0xA9; keyCode = 0xA9;
break; break;
case SDLK_AC_SEARCH: case SDL_SCANCODE_AC_SEARCH:
keyCode = 0xAA; keyCode = 0xAA;
break; break;
case SDLK_AC_BOOKMARKS: case SDL_SCANCODE_AC_BOOKMARKS:
keyCode = 0xAB; keyCode = 0xAB;
break; break;
case SDLK_AC_HOME: case SDL_SCANCODE_AC_HOME:
keyCode = 0xAC; keyCode = 0xAC;
break; break;
case SDLK_SEMICOLON: case SDL_SCANCODE_SEMICOLON:
keyCode = 0xBA; keyCode = 0xBA;
break; break;
case SDLK_PLUS: case SDL_SCANCODE_EQUALS:
keyCode = 0xBB; keyCode = 0xBB;
break; break;
case SDLK_COMMA: case SDL_SCANCODE_COMMA:
keyCode = 0xBC; keyCode = 0xBC;
break; break;
case SDLK_MINUS: case SDL_SCANCODE_MINUS:
keyCode = 0xBD; keyCode = 0xBD;
break; break;
case SDLK_PERIOD: case SDL_SCANCODE_PERIOD:
keyCode = 0xBE; keyCode = 0xBE;
break; break;
case SDLK_SLASH: case SDL_SCANCODE_SLASH:
keyCode = 0xBF; keyCode = 0xBF;
break; break;
case SDLK_BACKQUOTE: case SDL_SCANCODE_GRAVE:
keyCode = 0xC0; keyCode = 0xC0;
break; break;
case SDLK_LEFTBRACKET: case SDL_SCANCODE_LEFTBRACKET:
keyCode = 0xDB; keyCode = 0xDB;
break; break;
case SDLK_BACKSLASH: case SDL_SCANCODE_BACKSLASH:
keyCode = 0xDC; keyCode = 0xDC;
break; break;
case SDLK_RIGHTBRACKET: case SDL_SCANCODE_RIGHTBRACKET:
keyCode = 0xDD; keyCode = 0xDD;
break; break;
case SDLK_QUOTE: case SDL_SCANCODE_APOSTROPHE:
keyCode = 0xDE; keyCode = 0xDE;
break; break;
case SDL_SCANCODE_NONUSBACKSLASH:
keyCode = 0xE2;
break;
default: default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d", "Unhandled button event: %d",
event->keysym.sym); event->keysym.scancode);
return; return;
} }
} }
@@ -318,9 +394,13 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
{ {
int button; int button;
// Capture the mouse again if clicked when unbound // Capture the mouse again if clicked when unbound.
// We start capture on left button released instead of
// pressed to avoid sending an errant mouse button released
// event to the host when clicking into our window (since
// the pressed event was consumed by this code).
if (event->button == SDL_BUTTON_LEFT && if (event->button == SDL_BUTTON_LEFT &&
event->state == SDL_PRESSED && event->state == SDL_RELEASED &&
!SDL_GetRelativeMouseMode()) { !SDL_GetRelativeMouseMode()) {
SDL_SetRelativeMouseMode(SDL_TRUE); SDL_SetRelativeMouseMode(SDL_TRUE);
return; return;
@@ -364,10 +444,39 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
// Not capturing // Not capturing
return; return;
} }
short xdelta = (short)event->xrel;
short ydelta = (short)event->yrel;
if (event->xrel != 0 || event->yrel != 0) { // If we're sending more than one motion event per millisecond,
LiSendMouseMoveEvent((unsigned short)event->xrel, // delay for 1 ms to allow batching of mouse move events. On older
(unsigned short)event->yrel); // versions of GFE, we will unconditionally wait this 1 ms to
// work around an input processing issue that causes massive mouse latency.
Uint32 currentTime = SDL_GetTicks();
if (m_NeedsInputDelay || !SDL_TICKS_PASSED(currentTime, m_LastMouseMotionTime + 1)) {
SDL_Delay(1);
currentTime = SDL_GetTicks();
}
m_LastMouseMotionTime = currentTime;
// Pump even if we didn't delay since we might get some extra events
SDL_PumpEvents();
// Batch all of the pending mouse motion events
SDL_Event nextEvent;
while (SDL_PeepEvents(&nextEvent,
1,
SDL_GETEVENT,
SDL_MOUSEMOTION,
SDL_MOUSEMOTION) == 1) {
// In theory, these can overflow but in practice
// it should be highly unlikely since it would require
// moving 64K pixels in 1 ms.
xdelta += nextEvent.motion.xrel;
ydelta += nextEvent.motion.yrel;
}
if (xdelta != 0 || ydelta != 0) {
LiSendMouseMoveEvent(xdelta, ydelta);
} }
} }
@@ -427,13 +536,18 @@ void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
state->lsX = event->value; state->lsX = event->value;
break; break;
case SDL_CONTROLLER_AXIS_LEFTY: case SDL_CONTROLLER_AXIS_LEFTY:
state->lsY = -event->value; // Signed values have one more negative value than
// positive value, so inverting the sign on -32768
// could actually cause the value to overflow and
// wrap around to be negative again. Avoid that by
// capping the value at 32767.
state->lsY = -qMax(event->value, (short)-32767);
break; break;
case SDL_CONTROLLER_AXIS_RIGHTX: case SDL_CONTROLLER_AXIS_RIGHTX:
state->rsX = event->value; state->rsX = event->value;
break; break;
case SDL_CONTROLLER_AXIS_RIGHTY: case SDL_CONTROLLER_AXIS_RIGHTY:
state->rsY = -event->value; state->rsY = -qMax(event->value, (short)-32767);
break; break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT: case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
state->lt = (unsigned char)(event->value * 255UL / 32767); state->lt = (unsigned char)(event->value * 255UL / 32767);
+6 -1
View File
@@ -1,5 +1,8 @@
#pragma once #pragma once
#include "settings/streamingpreferences.h"
#include "backend/computermanager.h"
#include <SDL.h> #include <SDL.h>
struct GamepadState { struct GamepadState {
@@ -18,7 +21,7 @@ struct GamepadState {
class SdlInputHandler class SdlInputHandler
{ {
public: public:
explicit SdlInputHandler(bool multiController); explicit SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer);
~SdlInputHandler(); ~SdlInputHandler();
@@ -44,7 +47,9 @@ private:
void sendGamepadState(GamepadState* state); void sendGamepadState(GamepadState* state);
Uint32 m_LastMouseMotionTime;
bool m_MultiController; bool m_MultiController;
bool m_NeedsInputDelay;
int m_GamepadMask; int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS]; GamepadState m_GamepadState[MAX_GAMEPADS];
+686 -131
View File
File diff suppressed because it is too large. Load diff
+55 -31
View File
@@ -8,6 +8,7 @@
#include "settings/streamingpreferences.h" #include "settings/streamingpreferences.h"
#include "input.hpp" #include "input.hpp"
#include "video/decoder.h" #include "video/decoder.h"
#include "audio/renderers/renderer.h"
class Session : public QObject class Session : public QObject
{ {
@@ -19,7 +20,15 @@ class Session : public QObject
public: public:
explicit Session(NvComputer* computer, NvApp& app); explicit Session(NvComputer* computer, NvApp& app);
Q_INVOKABLE void exec(); Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
signals: signals:
void stageStarting(QString stage); void stageStarting(QString stage);
@@ -33,20 +42,33 @@ signals:
void displayLaunchWarning(QString text); void displayLaunchWarning(QString text);
private: private:
void initialize();
bool validateLaunch(); bool validateLaunch();
void emitLaunchWarning(QString text);
int getDecoderCapabilities(); int getDecoderCapabilities();
int sdlDetermineAudioConfiguration(); IAudioRenderer* createAudioRenderer();
static int detectAudioConfiguration();
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate); void cleanupAudioRendererOnMainThread();
bool testAudio(int audioConfiguration);
void getWindowDimensions(int& x, int& y,
int& width, int& height);
void toggleFullscreen();
void updateOptimalWindowDisplayMode();
static static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height, SDL_Window* window, int videoFormat, int width, int height,
int frameRate, IVideoDecoder*& chosenDecoder); int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder);
static static
void clStageStarting(int stage); void clStageStarting(int stage);
@@ -60,6 +82,17 @@ private:
static static
void clLogMessage(const char* format, ...); void clLogMessage(const char* format, ...);
static
int arInit(int audioConfiguration,
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* arContext, int arFlags);
static
void arCleanup();
static
void arDecodeAndPlaySample(char* sampleData, int sampleLength);
static static
int drSetup(int videoFormat, int width, int height, int frameRate, void*, int); int drSetup(int videoFormat, int width, int height, int frameRate, void*, int);
@@ -69,23 +102,6 @@ private:
static static
int drSubmitDecodeUnit(PDECODE_UNIT du); int drSubmitDecodeUnit(PDECODE_UNIT du);
static
int sdlAudioInit(int audioConfiguration,
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* arContext, int arFlags);
static
void sdlAudioStart();
static
void sdlAudioStop();
static
void sdlAudioCleanup();
static
void sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength);
StreamingPreferences m_Preferences; StreamingPreferences m_Preferences;
STREAM_CONFIGURATION m_StreamConfig; STREAM_CONFIGURATION m_StreamConfig;
DECODER_RENDERER_CALLBACKS m_VideoCallbacks; DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
@@ -94,15 +110,23 @@ private:
SDL_Window* m_Window; SDL_Window* m_Window;
IVideoDecoder* m_VideoDecoder; IVideoDecoder* m_VideoDecoder;
SDL_SpinLock m_DecoderLock; SDL_SpinLock m_DecoderLock;
bool m_NeedsIdr;
bool m_AudioDisabled;
Uint32 m_FullScreenFlag;
int m_DisplayOriginX;
int m_DisplayOriginY;
bool m_PendingWindowedTransition;
static SDL_AudioDeviceID s_AudioDevice; int m_ActiveVideoFormat;
static OpusMSDecoder* s_OpusDecoder; int m_ActiveVideoWidth;
static short s_OpusDecodeBuffer[]; int m_ActiveVideoHeight;
static int s_ChannelCount; int m_ActiveVideoFrameRate;
static int s_PendingDrops;
static int s_PendingHardDrops; OpusMSDecoder* m_OpusDecoder;
static unsigned int s_SampleIndex; short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
static Uint32 s_BaselinePendingData; IAudioRenderer* m_AudioRenderer;
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
SDL_SpinLock m_AudioRendererLock;
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks; static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks; static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
+127
View File
@@ -0,0 +1,127 @@
#include "streamutils.h"
#include <Qt>
#ifdef Q_OS_DARWIN
#include <ApplicationServices/ApplicationServices.h>
#endif
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
{
int dstH = dst->w * src->h / src->w;
int dstW = dst->h * src->w / src->h;
if (dstH > dst->h) {
dst->y = 0;
dst->x = (dst->w - dstW) / 2;
dst->w = dstW;
SDL_assert(dst->w * src->h / src->w <= dst->h);
}
else {
dst->x = 0;
dst->y = (dst->h - dstH) / 2;
dst->h = dstH;
SDL_assert(dst->h * src->w / src->h <= dst->w);
}
}
int StreamUtils::getDisplayRefreshRate(SDL_Window* window)
{
int displayIndex = SDL_GetWindowDisplayIndex(window);
if (displayIndex < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to get current display: %s",
SDL_GetError());
// Assume display 0 if it fails
displayIndex = 0;
}
SDL_DisplayMode mode;
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
// Use the window display mode for full-screen exclusive mode
if (SDL_GetWindowDisplayMode(window, &mode) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowDisplayMode() failed: %s",
SDL_GetError());
// Assume 60 Hz
return 60;
}
}
else {
// Use the current display mode for windowed and borderless
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetCurrentDisplayMode() failed: %s",
SDL_GetError());
// Assume 60 Hz
return 60;
}
}
// May be zero if undefined
if (mode.refresh_rate == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Refresh rate unknown; assuming 60 Hz");
mode.refresh_rate = 60;
}
return mode.refresh_rate;
}
bool StreamUtils::getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode)
{
#ifdef Q_OS_DARWIN
#define MAX_DISPLAYS 16
CGDirectDisplayID displayIds[MAX_DISPLAYS];
uint32_t displayCount = 0;
CGGetActiveDisplayList(MAX_DISPLAYS, displayIds, &displayCount);
if (displayIndex >= displayCount) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Too many displays: %d vs %d",
displayIndex, displayCount);
return false;
}
SDL_zerop(mode);
// Retina displays have non-native resolutions both below and above (!) their
// native resolution, so it's impossible for us to figure out what's actually
// native on macOS using the SDL API alone. We'll talk to CoreGraphics to
// find the correct resolution and match it in our SDL list.
CFArrayRef modeList = CGDisplayCopyAllDisplayModes(displayIds[displayIndex], nullptr);
CFIndex count = CFArrayGetCount(modeList);
for (CFIndex i = 0; i < count; i++) {
auto cgMode = (CGDisplayModeRef)(CFArrayGetValueAtIndex(modeList, i));
if ((CGDisplayModeGetIOFlags(cgMode) & kDisplayModeNativeFlag) != 0) {
mode->w = static_cast<int>(CGDisplayModeGetWidth(cgMode));
mode->h = static_cast<int>(CGDisplayModeGetHeight(cgMode));
break;
}
}
CFRelease(modeList);
// Now find the SDL mode that matches the CG native mode
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
SDL_DisplayMode thisMode;
if (SDL_GetDisplayMode(displayIndex, i, &thisMode) == 0) {
if (thisMode.w == mode->w && thisMode.h == mode->h &&
thisMode.refresh_rate >= mode->refresh_rate) {
*mode = thisMode;
break;
}
}
}
#else
if (SDL_GetDesktopDisplayMode(displayIndex, mode) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return false;
}
#endif
return true;
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <SDL.h>
class StreamUtils
{
public:
static
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
static
bool getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode);
static
int getDisplayRefreshRate(SDL_Window* window);
};
-11
View File
@@ -1,11 +0,0 @@
#pragma once
#include "decoder.h"
// A factory is required to avoid pulling in
// incompatible Objective-C headers.
class AVFoundationDecoderFactory {
public:
static
IVideoDecoder* createDecoder();
};
-521
View File
@@ -1,521 +0,0 @@
#include "avfoundation.h"
#include <SDL_syswm.h>
#import <Cocoa/Cocoa.h>
#import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreVideo/CoreVideo.h>
#define FRAME_START_PREFIX_SIZE 4
#define NALU_START_PREFIX_SIZE 3
#define NAL_LENGTH_PREFIX_SIZE 4
#define FUTURE_FRAMES_IN_VSYNC 3
class AVFoundationDecoder : public IVideoDecoder
{
public:
AVFoundationDecoder()
: m_DisplayLayer(nullptr),
m_FormatDesc(nullptr),
m_SpsData(nullptr),
m_PpsData(nullptr),
m_VpsData(nullptr),
m_View(nullptr),
m_NeedsIdr(false),
m_DisplayLink(nullptr),
m_VsyncTime(0),
m_LastVsyncTime(0),
m_VsyncStatsLock(0)
{
SDL_zero(m_FrameCountOnVsync);
}
virtual ~AVFoundationDecoder()
{
if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc);
}
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
delete m_SpsData;
delete m_PpsData;
delete m_VpsData;
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef,
const CVTimeStamp*,
const CVTimeStamp* vsyncTime,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
AVFoundationDecoder* me = reinterpret_cast<AVFoundationDecoder*>(displayLinkContext);
SDL_AtomicLock(&me->m_VsyncStatsLock);
me->m_LastVsyncTime = me->m_VsyncTime;
me->m_VsyncTime = vsyncTime->hostTime;
int framesThisVsync = *(volatile int*)&me->m_FrameCountOnVsync[0];
memmove(&me->m_FrameCountOnVsync[0],
&me->m_FrameCountOnVsync[1],
sizeof(me->m_FrameCountOnVsync) - sizeof(me->m_FrameCountOnVsync[0]));
me->m_FrameCountOnVsync[FUTURE_FRAMES_IN_VSYNC - 1] = 0;
SDL_AtomicUnlock(&me->m_VsyncStatsLock);
if (framesThisVsync != 1)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync interval frames: %d",
framesThisVsync);
return kCVReturnSuccess;
}
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int, int, int) override
{
m_VideoFormat = videoFormat;
// Fail if the user wants software decode only
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
return false;
}
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
// Prior to 10.13, we'll just assume everything has
// H.264 support and fail open to allow VT decode.
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
if (__builtin_available(macOS 10.13, *)) {
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_H264)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HW accelerated H.264 decode via VT");
return false;
}
}
else
#endif
{
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Assuming H.264 HW decode on < macOS 10.13");
}
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
if (__builtin_available(macOS 10.13, *)) {
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HW accelerated HEVC decode via VT");
return false;
}
}
else
#endif
{
// Fail closed for HEVC if we're not on 10.13+
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC support on < macOS 10.13");
return false;
}
}
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
// SDL adds its own content view to listen for events.
// We need to add a subview for our display layer.
NSView* contentView = info.info.cocoa.window.contentView;
m_View = [[NSView alloc] initWithFrame:contentView.bounds];
m_View.wantsLayer = YES;
[contentView addSubview: m_View];
setupDisplayLayer();
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, (void*)this);
CVDisplayLinkStart(m_DisplayLink);
return true;
}
virtual bool isHardwareAccelerated() override
{
// We only use AVFoundation for HW acceleration
return true;
}
virtual int submitDecodeUnit(PDECODE_UNIT du) override
{
OSStatus status;
unsigned char* data = (unsigned char*)malloc(du->fullLength);
if (!data) {
return DR_NEED_IDR;
}
PLENTRY entry = du->bufferList;
int pictureDataLength = 0;
while (entry != nullptr) {
if (entry->bufferType == BUFFER_TYPE_VPS) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got VPS");
delete m_VpsData;
m_VpsData = new QByteArray(&entry->data[FRAME_START_PREFIX_SIZE], entry->length - FRAME_START_PREFIX_SIZE);
// We got a new VPS so wait for a new SPS to match it
delete m_SpsData;
delete m_PpsData;
m_SpsData = m_PpsData = nullptr;
}
else if (entry->bufferType == BUFFER_TYPE_SPS) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got SPS");
delete m_SpsData;
m_SpsData = new QByteArray(&entry->data[FRAME_START_PREFIX_SIZE], entry->length - FRAME_START_PREFIX_SIZE);
// We got a new SPS so wait for a new PPS to match it
delete m_PpsData;
m_PpsData = nullptr;
}
else if (entry->bufferType == BUFFER_TYPE_PPS) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got PPS");
delete m_PpsData;
m_PpsData = new QByteArray(&entry->data[FRAME_START_PREFIX_SIZE], entry->length - FRAME_START_PREFIX_SIZE);
}
else {
// Picture data
SDL_assert(entry->bufferType == BUFFER_TYPE_PICDATA);
memcpy(&data[pictureDataLength],
entry->data,
entry->length);
pictureDataLength += entry->length;
}
entry = entry->next;
}
SDL_assert(pictureDataLength <= du->fullLength);
if (du->frameType == FRAME_TYPE_IDR) {
m_NeedsIdr = false;
if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc);
}
if (m_VideoFormat & VIDEO_FORMAT_MASK_H264) {
const uint8_t* const parameterSetPointers[] = { (uint8_t*)m_SpsData->data(),
(uint8_t*)m_PpsData->data() };
const size_t parameterSetSizes[] = { (size_t)m_SpsData->length(),
(size_t)m_PpsData->length() };
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Constructing new H264 format description");
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
2, /* count of parameter sets */
parameterSetPointers,
parameterSetSizes,
NAL_LENGTH_PREFIX_SIZE,
&m_FormatDesc);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create H264 format description: %d",
(int)status);
m_FormatDesc = NULL;
}
}
else {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
const uint8_t* const parameterSetPointers[] = { (uint8_t*)m_VpsData->data(),
(uint8_t*)m_SpsData->data(),
(uint8_t*)m_PpsData->data() };
const size_t parameterSetSizes[] = { (size_t)m_VpsData->length(),
(size_t)m_SpsData->length(),
(size_t)m_PpsData->length() };
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Constructing new HEVC format description");
if (__builtin_available(macOS 10.13, *)) {
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
3, /* count of parameter sets */
parameterSetPointers,
parameterSetSizes,
NAL_LENGTH_PREFIX_SIZE,
nil,
&m_FormatDesc);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create HEVC format description: %d",
(int)status);
m_FormatDesc = NULL;
}
}
else
#endif
{
// This means Moonlight-common-c decided to give us an HEVC stream
// even though we said we couldn't support it. All we can do is abort().
abort();
}
}
}
if (m_FormatDesc == nullptr || m_NeedsIdr) {
// Can't decode if we haven't gotten our parameter sets yet
free(data);
return DR_NEED_IDR;
}
// Now we're decoding actual frame data here
CMBlockBufferRef blockBuffer;
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &blockBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferCreateEmpty failed: %d",
(int)status);
free(data);
return DR_NEED_IDR;
}
int lastOffset = -1;
for (int i = 0; i < pictureDataLength - FRAME_START_PREFIX_SIZE; i++) {
// Search for a NALU
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
// It's the start of a new NALU
if (lastOffset != -1) {
// We've seen a start before this so enqueue that NALU
updateBufferForRange(blockBuffer, data, lastOffset, i - lastOffset);
}
lastOffset = i;
}
}
if (lastOffset != -1) {
// Enqueue the remaining data
updateBufferForRange(blockBuffer, data, lastOffset, pictureDataLength - lastOffset);
}
// From now on, CMBlockBuffer owns the data pointer and will free it when it's released
CMSampleBufferRef sampleBuffer;
CMSampleTimingInfo time = {
.decodeTimeStamp = kCMTimeInvalid,
.duration = kCMTimeInvalid
};
SDL_AtomicLock(&m_VsyncStatsLock);
// Find a slot to put this frame
for (int i = 0; i < FUTURE_FRAMES_IN_VSYNC; i++) {
if (m_FrameCountOnVsync[i] == 0) {
time.presentationTimeStamp =
CMTimeMake(m_VsyncTime + (i + 1) * (m_VsyncTime - m_LastVsyncTime),
1000 * 1000 * 1000);
m_FrameCountOnVsync[i]++;
}
}
SDL_AtomicUnlock(&m_VsyncStatsLock);
status = CMSampleBufferCreate(kCFAllocatorDefault,
blockBuffer,
true, NULL,
NULL, m_FormatDesc, 1, 1,
&time, 0, NULL,
&sampleBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMSampleBufferCreate failed: %d",
(int)status);
CFRelease(blockBuffer);
return DR_NEED_IDR;
}
// Submit for callback on the main thread
queueFrame(sampleBuffer, (void*)(long)du->frameType);
return DR_OK;
}
// This is called on the main thread, so it's safe to interact with
// the AVSampleBufferDisplayLayer here.
void submitBuffer(CMSampleBufferRef sampleBuffer, int frameType)
{
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Resetting failed AVSampleBufferDisplay layer");
setupDisplayLayer();
CFRelease(sampleBuffer);
return;
}
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_IsDependedOnByOthers, kCFBooleanTrue);
if (frameType == FRAME_TYPE_PFRAME) {
// P-frame
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanTrue);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanTrue);
} else {
// I-frame
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanFalse);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
}
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
}
virtual void renderFrame(SDL_UserEvent* event) override
{
submitBuffer((CMSampleBufferRef)event->data1, (int)(long)event->data2);
}
virtual void dropFrame(SDL_UserEvent* event) override
{
// Submit anyway and let the drop happen in the render pipeline
submitBuffer((CMSampleBufferRef)event->data1, (int)(long)event->data2);
}
private:
void setupDisplayLayer()
{
CALayer* oldLayer = m_DisplayLayer;
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
m_DisplayLayer.bounds = m_View.bounds;
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds));
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
CALayer* viewLayer = m_View.layer;
if (oldLayer != nil) {
[viewLayer replaceSublayer:oldLayer with:m_DisplayLayer];
}
else {
[viewLayer addSublayer:m_DisplayLayer];
}
// Wait for another IDR frame
m_NeedsIdr = true;
delete m_SpsData;
delete m_PpsData;
delete m_VpsData;
m_SpsData = m_PpsData = m_VpsData = nullptr;
}
void updateBufferForRange(CMBlockBufferRef existingBuffer, unsigned char* data, int offset, int nalLength)
{
OSStatus status;
size_t oldOffset = CMBlockBufferGetDataLength(existingBuffer);
// If we're at index 1 (first NALU in frame), enqueue this buffer to the memory block
// so it can handle freeing it when the block buffer is destroyed
if (offset == 1) {
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
// Pass the real buffer pointer directly (no offset)
// This will give it to the block buffer to free when it's released.
// All further calls to CMBlockBufferAppendMemoryBlock will do so
// at an offset and will not be asking the buffer to be freed.
status = CMBlockBufferAppendMemoryBlock(existingBuffer, data,
nalLength + 1, // Add 1 for the offset we decremented
kCFAllocatorDefault,
NULL, 0, nalLength + 1, 0);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferReplaceDataBytes failed: %d",
(int)status);
return;
}
// Write the length prefix to existing buffer
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
oldOffset, NAL_LENGTH_PREFIX_SIZE);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferReplaceDataBytes failed: %d",
(int)status);
return;
}
} else {
// Append a 4 byte buffer to this block for the length prefix
status = CMBlockBufferAppendMemoryBlock(existingBuffer, NULL,
NAL_LENGTH_PREFIX_SIZE,
kCFAllocatorDefault, NULL, 0,
NAL_LENGTH_PREFIX_SIZE, 0);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferAppendMemoryBlock failed: %d",
(int)status);
return;
}
// Write the length prefix to the new buffer
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
oldOffset, NAL_LENGTH_PREFIX_SIZE);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferReplaceDataBytes failed: %d",
(int)status);
return;
}
// Attach the buffer by reference to the block buffer
status = CMBlockBufferAppendMemoryBlock(existingBuffer, &data[offset+NALU_START_PREFIX_SIZE],
dataLength,
kCFAllocatorNull, // Don't deallocate data on free
NULL, 0, dataLength, 0);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferReplaceDataBytes failed: %d",
(int)status);
return;
}
}
}
AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc;
int m_VideoFormat;
QByteArray* m_SpsData;
QByteArray* m_PpsData;
QByteArray* m_VpsData;
NSView* m_View;
bool m_NeedsIdr;
CVDisplayLinkRef m_DisplayLink;
uint64_t m_VsyncTime, m_LastVsyncTime;
int m_FrameCountOnVsync[FUTURE_FRAMES_IN_VSYNC];
SDL_SpinLock m_VsyncStatsLock;
};
IVideoDecoder* AVFoundationDecoderFactory::createDecoder() {
return new AVFoundationDecoder();
}
+3 -1
View File
@@ -16,8 +16,10 @@ public:
int videoFormat, int videoFormat,
int width, int width,
int height, int height,
int frameRate) = 0; int frameRate,
bool enableVsync) = 0;
virtual bool isHardwareAccelerated() = 0; virtual bool isHardwareAccelerated() = 0;
virtual int getDecoderCapabilities() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0; virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
virtual void renderFrame(SDL_UserEvent* event) = 0; virtual void renderFrame(SDL_UserEvent* event) = 0;
virtual void dropFrame(SDL_UserEvent* event) = 0; virtual void dropFrame(SDL_UserEvent* event) = 0;
+475 -94
View File
@@ -1,5 +1,14 @@
#include <Initguid.h> #include <initguid.h>
#include "dxva2.h" #include "dxva2.h"
#include "../ffmpeg.h"
#include <streaming/streamutils.h>
#include <SDL_syswm.h>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <VersionHelpers.h>
#include <dwmapi.h>
#include <Limelight.h> #include <Limelight.h>
@@ -8,23 +17,27 @@ DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0x
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); } #define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
DXVA2Renderer::DXVA2Renderer() : DXVA2Renderer::DXVA2Renderer() :
m_FrameIndex(0),
m_SurfacesUsed(0),
m_SdlRenderer(nullptr),
m_DecService(nullptr), m_DecService(nullptr),
m_Decoder(nullptr), m_Decoder(nullptr),
m_SurfacesUsed(0),
m_Pool(nullptr), m_Pool(nullptr),
m_Device(nullptr), m_Device(nullptr),
m_RenderTarget(nullptr), m_RenderTarget(nullptr),
m_ProcService(nullptr), m_ProcService(nullptr),
m_Processor(nullptr) m_Processor(nullptr),
m_FrameIndex(0)
{ {
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces)); RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext)); RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
// Use MMCSS scheduling for lower scheduling latency while we're streaming
DwmEnableMMCSS(TRUE);
} }
DXVA2Renderer::~DXVA2Renderer() DXVA2Renderer::~DXVA2Renderer()
{ {
DwmEnableMMCSS(FALSE);
SAFE_COM_RELEASE(m_DecService); SAFE_COM_RELEASE(m_DecService);
SAFE_COM_RELEASE(m_Decoder); SAFE_COM_RELEASE(m_Decoder);
SAFE_COM_RELEASE(m_Device); SAFE_COM_RELEASE(m_Device);
@@ -39,10 +52,6 @@ DXVA2Renderer::~DXVA2Renderer()
if (m_Pool != nullptr) { if (m_Pool != nullptr) {
av_buffer_pool_uninit(&m_Pool); av_buffer_pool_uninit(&m_Pool);
} }
if (m_SdlRenderer != nullptr) {
SDL_DestroyRenderer(m_SdlRenderer);
}
} }
void DXVA2Renderer::ffPoolDummyDelete(void*, uint8_t*) void DXVA2Renderer::ffPoolDummyDelete(void*, uint8_t*)
@@ -75,11 +84,8 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
context->hwaccel_context = &m_DXVAContext; context->hwaccel_context = &m_DXVAContext;
context->get_format = ffGetFormat;
context->get_buffer2 = ffGetBuffer2; context->get_buffer2 = ffGetBuffer2;
context->opaque = this;
m_Pool = av_buffer_pool_init2(ARRAYSIZE(m_DecSurfaces), this, ffPoolAlloc, nullptr); m_Pool = av_buffer_pool_init2(ARRAYSIZE(m_DecSurfaces), this, ffPoolAlloc, nullptr);
if (!m_Pool) { if (!m_Pool) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -87,12 +93,15 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
return false; return false;
} }
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using DXVA2 accelerated renderer");
return true; return true;
} }
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int) int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
{ {
DXVA2Renderer* me = reinterpret_cast<DXVA2Renderer*>(context->opaque); DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getRenderer();
frame->buf[0] = av_buffer_pool_get(me->m_Pool); frame->buf[0] = av_buffer_pool_get(me->m_Pool);
if (!frame->buf[0]) { if (!frame->buf[0]) {
@@ -101,31 +110,20 @@ int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
frame->data[3] = frame->buf[0]->data; frame->data[3] = frame->buf[0]->data;
frame->format = AV_PIX_FMT_DXVA2_VLD; frame->format = AV_PIX_FMT_DXVA2_VLD;
frame->width = me->m_Width; frame->width = me->m_VideoWidth;
frame->height = me->m_Height; frame->height = me->m_VideoHeight;
return 0; return 0;
} }
enum AVPixelFormat DXVA2Renderer::ffGetFormat(AVCodecContext*,
const enum AVPixelFormat* pixFmts)
{
const enum AVPixelFormat *p;
for (p = pixFmts; *p != -1; p++) {
if (*p == AV_PIX_FMT_DXVA2_VLD)
return *p;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to find DXVA2 HW surface format");
return AV_PIX_FMT_NONE;
}
bool DXVA2Renderer::initializeDecoder() bool DXVA2Renderer::initializeDecoder()
{ {
HRESULT hr; HRESULT hr;
if (isDecoderBlacklisted()) {
return false;
}
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoDecoderService, hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoDecoderService,
reinterpret_cast<void**>(&m_DecService)); reinterpret_cast<void**>(&m_DecService));
if (FAILED(hr)) { if (FAILED(hr)) {
@@ -208,18 +206,11 @@ bool DXVA2Renderer::initializeDecoder()
return false; return false;
} }
int alignment; // Alignment was already taken care of
SDL_assert(m_Desc.SampleWidth % 16 == 0);
// HEVC using DXVA requires 128B alignment SDL_assert(m_Desc.SampleHeight % 16 == 0);
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) { hr = m_DecService->CreateSurface(m_Desc.SampleWidth,
alignment = 128; m_Desc.SampleHeight,
}
else {
alignment = 16;
}
hr = m_DecService->CreateSurface(FFALIGN(m_Width, alignment),
FFALIGN(m_Height, alignment),
ARRAYSIZE(m_DecSurfaces) - 1, ARRAYSIZE(m_DecSurfaces) - 1,
m_Desc.Format, m_Desc.Format,
D3DPOOL_DEFAULT, D3DPOOL_DEFAULT,
@@ -262,6 +253,9 @@ bool DXVA2Renderer::initializeRenderer()
D3DSURFACE_DESC renderTargetDesc; D3DSURFACE_DESC renderTargetDesc;
m_RenderTarget->GetDesc(&renderTargetDesc); m_RenderTarget->GetDesc(&renderTargetDesc);
m_DisplayWidth = renderTargetDesc.Width;
m_DisplayHeight = renderTargetDesc.Height;
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService, hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
reinterpret_cast<void**>(&m_ProcService)); reinterpret_cast<void**>(&m_ProcService));
@@ -302,6 +296,14 @@ bool DXVA2Renderer::initializeRenderer()
caps.VideoProcessorOperations); caps.VideoProcessorOperations);
continue; continue;
} }
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device %d can't stretch video: %x",
i,
caps.VideoProcessorOperations);
continue;
}
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange); m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange); m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
@@ -338,45 +340,302 @@ bool DXVA2Renderer::initializeRenderer()
return true; return true;
} }
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height) bool DXVA2Renderer::isDecoderBlacklisted()
{ {
m_VideoFormat = videoFormat; IDirect3D9* d3d9;
m_Width = width; HRESULT hr;
m_Height = height; bool result = false;
// FFmpeg will be decoding on different threads than the main thread that we're // TODO: Update for HEVC Main10
// currently running on right now. We must set this hint so SDL will pass SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
// D3DCREATE_MULTITHREADED to IDirect3D9::CreateDevice().
SDL_SetHint(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1");
// We want VSYNC when running in full-screen. DWM will give us hr = m_Device->GetDirect3D(&d3d9);
// tear-free video when running composited (windowed) without any if (SUCCEEDED(hr)) {
// extra latency waiting for VSYNC. D3DCAPS9 caps;
m_SdlRenderer = SDL_CreateRenderer(window, -1,
SDL_RENDERER_ACCELERATED | hr = m_Device->GetDeviceCaps(&caps);
((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) ? if (SUCCEEDED(hr)) {
SDL_RENDERER_PRESENTVSYNC : 0)); D3DADAPTER_IDENTIFIER9 id;
if (!m_SdlRenderer) {
hr = d3d9->GetAdapterIdentifier(caps.AdapterOrdinal, 0, &id);
if (SUCCEEDED(hr)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected GPU: %s (%x:%x)",
id.Description,
id.VendorId,
id.DeviceId);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU driver: %s %d.%d.%d.%d",
id.Driver,
HIWORD(id.DriverVersion.HighPart),
LOWORD(id.DriverVersion.HighPart),
HIWORD(id.DriverVersion.LowPart),
LOWORD(id.DriverVersion.LowPart));
if (id.VendorId == 0x8086) {
// Intel seems to encode the series in the high byte of
// the device ID. We want to avoid the "Partial" acceleration
// support explicitly. Those will claim to have HW acceleration
// but perform badly.
// https://en.wikipedia.org/wiki/Intel_Graphics_Technology#Capabilities_(GPU_video_acceleration)
// https://raw.githubusercontent.com/GameTechDev/gpudetect/master/IntelGfx.cfg
switch (id.DeviceId & 0xFF00) {
case 0x0400: // Haswell
case 0x0A00: // Haswell
case 0x0D00: // Haswell
case 0x1600: // Broadwell
case 0x2200: // Cherry Trail and Braswell
// Blacklist these for HEVC to avoid hybrid decode
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for HEVC due to hybrid decode");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
break;
default:
// Intel drivers from before late-2017 had a bug that caused some strange artifacts
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
// is not affected on my Intel HD 515.
// https://github.com/moonlight-stream/moonlight-qt/issues/32
// https://www.intel.com/content/www/us/en/support/articles/000005654/graphics-drivers.html
if (LOWORD(id.DriverVersion.LowPart) < 4836) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Intel driver version blacklisted for HEVC");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
}
else {
// Everything else is fine with whatever it says it supports
result = false;
}
break;
}
}
else if (id.VendorId == 0x10DE) {
// For NVIDIA, we wait to avoid those GPUs with Feature Set E
// for HEVC decoding, since that's hybrid. It appears that Kepler GPUs
// also had some hybrid decode support (per DXVA2 Checker) so we'll
// blacklist those too.
// https://en.wikipedia.org/wiki/Nvidia_PureVideo
// https://bluesky23.yukishigure.com/en/dxvac/deviceInfo/decoder.html
// http://envytools.readthedocs.io/en/latest/hw/pciid.html (missing GM200)
if ((id.DeviceId >= 0x1180 && id.DeviceId <= 0x11BF) || // GK104
(id.DeviceId >= 0x11C0 && id.DeviceId <= 0x11FF) || // GK106
(id.DeviceId >= 0x0FC0 && id.DeviceId <= 0x0FFF) || // GK107
(id.DeviceId >= 0x1000 && id.DeviceId <= 0x103F) || // GK110/GK110B
(id.DeviceId >= 0x1280 && id.DeviceId <= 0x12BF) || // GK208
(id.DeviceId >= 0x1340 && id.DeviceId <= 0x137F) || // GM108
(id.DeviceId >= 0x1380 && id.DeviceId <= 0x13BF) || // GM107
(id.DeviceId >= 0x13C0 && id.DeviceId <= 0x13FF) || // GM204
(id.DeviceId >= 0x1617 && id.DeviceId <= 0x161A) || // GM204
(id.DeviceId == 0x1667) || // GM204
(id.DeviceId >= 0x17C0 && id.DeviceId <= 0x17FF)) { // GM200
// Avoid HEVC on Feature Set E GPUs
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for HEVC due to hybrid decode");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
}
else {
result = false;
}
}
else if (id.VendorId == 0x1002) {
// AMD doesn't seem to do hybrid acceleration?
result = false;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unrecognized vendor ID: %x",
id.VendorId);
}
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetDeviceCaps() failed: %x", hr);
}
d3d9->Release();
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s", "GetDirect3D() failed: %x", hr);
SDL_GetError()); }
if (result) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for format %x",
m_VideoFormat);
}
return result;
}
bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
{
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
SDL_GetWindowWMInfo(window, &info);
IDirect3D9Ex* d3d9ex;
HRESULT hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d9ex);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Direct3DCreate9Ex() failed: %x",
hr);
return false; return false;
} }
m_Device = SDL_RenderGetD3D9Device(m_SdlRenderer); int adapterIndex = SDL_Direct3D9GetAdapterIndex(SDL_GetWindowDisplayIndex(window));
Uint32 windowFlags = SDL_GetWindowFlags(window);
D3DCAPS9 deviceCaps;
d3d9ex->GetDeviceCaps(adapterIndex, D3DDEVTYPE_HAL, &deviceCaps);
D3DDISPLAYMODEEX currentMode;
currentMode.Size = sizeof(currentMode);
d3d9ex->GetAdapterDisplayModeEx(adapterIndex, &currentMode, nullptr);
D3DPRESENT_PARAMETERS d3dpp = {};
d3dpp.hDeviceWindow = info.info.win.window;
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
d3dpp.Windowed = false;
d3dpp.BackBufferWidth = currentMode.Width;
d3dpp.BackBufferHeight = currentMode.Height;
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
d3dpp.BackBufferFormat = currentMode.Format;
}
else {
d3dpp.Windowed = true;
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
SDL_GetWindowSize(window, (int*)&d3dpp.BackBufferWidth, (int*)&d3dpp.BackBufferHeight);
}
BOOL dwmEnabled;
DwmIsCompositionEnabled(&dwmEnabled);
if (d3dpp.Windowed && dwmEnabled) {
// If composition enabled, disable v-sync and let DWM manage things
// to reduce latency by avoiding double v-syncing.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
// If V-sync is enabled (not rendering faster than display),
// we can use FlipEx for more efficient swapping.
if (enableVsync) {
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
// continue while DWM is waiting to render the surface to the display.
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
d3dpp.BackBufferCount = 2;
}
else {
// With V-sync off, we won't use FlipEx because that will block while
// DWM is waiting to render our surface (effectively behaving like V-Sync).
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Windowed mode with DWM running");
}
else if (enableVsync) {
// Uncomposited desktop or full-screen exclusive mode with V-sync enabled
// We will enable V-sync in this scenario to avoid tearing.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync enabled");
}
else {
// Uncomposited desktop or full-screen exclusive mode with V-sync disabled
// We will allowing tearing for lowest latency.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync disabled in tearing mode");
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Windowed: %d | Present Interval: %x",
d3dpp.Windowed, d3dpp.PresentationInterval);
// FFmpeg requires this attribute for doing asynchronous decoding
// in a separate thread with this device.
int deviceFlags = D3DCREATE_MULTITHREADED;
if (deviceCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) {
deviceFlags |= D3DCREATE_HARDWARE_VERTEXPROCESSING;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No hardware vertex processing support!");
deviceFlags |= D3DCREATE_SOFTWARE_VERTEXPROCESSING;
}
hr = d3d9ex->CreateDeviceEx(adapterIndex,
D3DDEVTYPE_HAL,
d3dpp.hDeviceWindow,
deviceFlags,
&d3dpp,
d3dpp.Windowed ? nullptr : &currentMode,
&m_Device);
d3d9ex->Release();
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CreateDeviceEx() failed: %x",
hr);
return false;
}
hr = m_Device->SetMaximumFrameLatency(1);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SetMaximumFrameLatency() failed: %x",
hr);
return false;
}
return true;
}
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height, int, bool enableVsync)
{
m_VideoFormat = videoFormat;
m_VideoWidth = width;
m_VideoHeight = height;
RtlZeroMemory(&m_Desc, sizeof(m_Desc)); RtlZeroMemory(&m_Desc, sizeof(m_Desc));
m_Desc.SampleWidth = m_Width;
m_Desc.SampleHeight = m_Height; int alignment;
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_ProgressiveChroma;
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255; // HEVC using DXVA requires 128B alignment
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709; if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_dim; alignment = 128;
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709; }
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709; else {
alignment = 16;
}
m_Desc.SampleWidth = FFALIGN(m_VideoWidth, alignment);
m_Desc.SampleHeight = FFALIGN(m_VideoHeight, alignment);
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_Unknown;
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame; m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2'); m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
if (!initializeDevice(window, enableVsync)) {
return false;
}
if (!initializeDecoder()) { if (!initializeDecoder()) {
return false; return false;
} }
@@ -385,51 +644,158 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
return false; return false;
} }
// For some reason, using Direct3D9Ex breaks this with multi-monitor setups.
// When focus is lost, the window is minimized then immediately restored without
// input focus. This glitches out the renderer and a bunch of other stuff.
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
// own, so just disable SDL's handling of the focus loss event.
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
return true; return true;
} }
void DXVA2Renderer::renderFrame(AVFrame* frame) bool DXVA2Renderer::needsTestFrame()
{
// We validate the DXVA2 profiles are supported
// in initialize() so no test frame is required
return false;
}
int DXVA2Renderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
{ {
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]); IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
HRESULT hr; HRESULT hr;
hr = m_Device->TestCooperativeLevel(); switch (frame->color_range) {
switch (hr) { case AVCOL_RANGE_JPEG:
case D3D_OK: m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
break;
case AVCOL_RANGE_MPEG:
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_16_235;
break; break;
case D3DERR_DEVICELOST:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3D device lost!");
return;
case D3DERR_DEVICENOTRESET:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3D device not reset!");
return;
default: default:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
"Unknown D3D error: %x", break;
hr); }
return;
switch (frame->color_primaries) {
case AVCOL_PRI_BT709:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709;
break;
case AVCOL_PRI_BT470M:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT470_2_SysM;
break;
case AVCOL_PRI_BT470BG:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT470_2_SysBG;
break;
case AVCOL_PRI_SMPTE170M:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_SMPTE170M;
break;
case AVCOL_PRI_SMPTE240M:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_SMPTE240M;
break;
default:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
break;
}
switch (frame->color_trc) {
case AVCOL_TRC_SMPTE170M:
case AVCOL_TRC_BT709:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709;
break;
case AVCOL_TRC_LINEAR:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_10;
break;
case AVCOL_TRC_GAMMA22:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_22;
break;
case AVCOL_TRC_GAMMA28:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_28;
break;
case AVCOL_TRC_SMPTE240M:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_240M;
break;
case AVCOL_TRC_IEC61966_2_1:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_sRGB;
break;
default:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
break;
}
switch (frame->colorspace) {
case AVCOL_SPC_BT709:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709;
break;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT601;
break;
case AVCOL_SPC_SMPTE240M:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_SMPTE240M;
break;
default:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
break;
}
switch (frame->chroma_location) {
case AVCHROMA_LOC_LEFT:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Horizontally_Cosited |
DXVA2_VideoChromaSubsampling_Vertically_AlignedChromaPlanes;
break;
case AVCHROMA_LOC_CENTER:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Vertically_AlignedChromaPlanes;
break;
case AVCHROMA_LOC_TOPLEFT:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Horizontally_Cosited |
DXVA2_VideoChromaSubsampling_Vertically_Cosited;
break;
default:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
break;
} }
DXVA2_VideoSample sample = {}; DXVA2_VideoSample sample = {};
sample.Start = m_FrameIndex; sample.Start = m_FrameIndex;
sample.End = m_FrameIndex + 1; sample.End = m_FrameIndex + 1;
sample.SrcSurface = surface; sample.SrcSurface = surface;
sample.SrcRect.right = m_Desc.SampleWidth; sample.SrcRect.right = m_VideoWidth;
sample.SrcRect.bottom = m_Desc.SampleHeight; sample.SrcRect.bottom = m_VideoHeight;
sample.DstRect = sample.SrcRect;
sample.SampleFormat = m_Desc.SampleFormat; sample.SampleFormat = m_Desc.SampleFormat;
sample.PlanarAlpha = DXVA2_Fixed32OpaqueAlpha(); sample.PlanarAlpha = DXVA2_Fixed32OpaqueAlpha();
// Center in frame and preserve aspect ratio
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = m_VideoWidth;
src.h = m_VideoHeight;
dst.x = dst.y = 0;
dst.w = m_DisplayWidth;
dst.h = m_DisplayHeight;
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
sample.DstRect.left = dst.x;
sample.DstRect.right = dst.x + dst.w;
sample.DstRect.top = dst.y;
sample.DstRect.bottom = dst.y + dst.h;
DXVA2_VideoProcessBltParams bltParams = {}; DXVA2_VideoProcessBltParams bltParams = {};
bltParams.TargetFrame = m_FrameIndex++; bltParams.TargetFrame = m_FrameIndex++;
bltParams.TargetRect.right = m_Desc.SampleWidth; bltParams.TargetRect = sample.DstRect;
bltParams.TargetRect.bottom = m_Desc.SampleHeight;
bltParams.BackgroundColor.Y = 0x1000;
bltParams.BackgroundColor.Cb = 0x8000;
bltParams.BackgroundColor.Cr = 0x8000;
bltParams.BackgroundColor.Alpha = 0xFFFF; bltParams.BackgroundColor.Alpha = 0xFFFF;
bltParams.DestFormat.SampleFormat = DXVA2_SampleProgressiveFrame; bltParams.DestFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
@@ -441,12 +807,27 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha(); bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr); hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VideoProcessBlt() failed: %x", "VideoProcessBlt() failed: %x",
hr); hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
} }
m_Device->Present(nullptr, nullptr, nullptr, nullptr); hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, 0);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PresentEx() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
} }
+15 -10
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "renderer.h" #include "renderer.h"
#include "pacer/pacer.h"
#include <d3d9.h> #include <d3d9.h>
#include <dxva2api.h> #include <dxva2api.h>
@@ -17,13 +18,20 @@ public:
virtual bool initialize(SDL_Window* window, virtual bool initialize(SDL_Window* window,
int videoFormat, int videoFormat,
int width, int width,
int height); int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context); virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame); virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private: private:
bool initializeDecoder(); bool initializeDecoder();
bool initializeRenderer(); bool initializeRenderer();
bool initializeDevice(SDL_Window* window, bool enableVsync);
bool isDecoderBlacklisted();
static static
AVBufferRef* ffPoolAlloc(void* opaque, int size); AVBufferRef* ffPoolAlloc(void* opaque, int size);
@@ -34,15 +42,12 @@ private:
static static
int ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int flags); int ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int flags);
static
enum AVPixelFormat ffGetFormat(AVCodecContext*,
const enum AVPixelFormat* pixFmts);
int m_VideoFormat; int m_VideoFormat;
int m_Width; int m_VideoWidth;
int m_Height; int m_VideoHeight;
SDL_Renderer* m_SdlRenderer; int m_DisplayWidth;
int m_DisplayHeight;
struct dxva_context m_DXVAContext; struct dxva_context m_DXVAContext;
IDirect3DSurface9* m_DecSurfaces[19]; IDirect3DSurface9* m_DecSurfaces[19];
@@ -52,7 +57,7 @@ private:
int m_SurfacesUsed; int m_SurfacesUsed;
AVBufferPool* m_Pool; AVBufferPool* m_Pool;
IDirect3DDevice9* m_Device; IDirect3DDevice9Ex* m_Device;
IDirect3DSurface9* m_RenderTarget; IDirect3DSurface9* m_RenderTarget;
IDirectXVideoProcessorService* m_ProcService; IDirectXVideoProcessorService* m_ProcService;
IDirectXVideoProcessor* m_Processor; IDirectXVideoProcessor* m_Processor;
@@ -0,0 +1,11 @@
#pragma once
#include "pacer.h"
class DisplayLinkVsyncSourceFactory
{
public:
static
IVsyncSource* createVsyncSource(Pacer* pacer);
};
@@ -0,0 +1,132 @@
#include "displaylinkvsyncsource.h"
#include <SDL_syswm.h>
#include <CoreVideo/CoreVideo.h>
#import <Cocoa/Cocoa.h>
class DisplayLinkVsyncSource : public IVsyncSource
{
public:
DisplayLinkVsyncSource(Pacer* pacer)
: m_Pacer(pacer),
m_DisplayLink(nullptr)
{
}
virtual ~DisplayLinkVsyncSource() override
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
}
static
CGDirectDisplayID
getDisplayID(NSScreen* screen)
{
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
return [screenNumber unsignedIntValue];
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef displayLink,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
SDL_assert(displayLink == me->m_DisplayLink);
// In my testing on macOS 10.13, this callback is invoked about 24 ms
// prior to the specified v-sync time (now - vsyncTime). Since this is
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
// draw using the current host time, rather than the actual v-sync target
// time. Because the CVDisplayLink is in sync with the actual v-sync
// interval, even if many ms prior, we can safely use the current host time
// and get a consistent callback for each v-sync. This reduces video latency
// by at least 1 frame vs. rendering with the actual vsyncTime.
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
return kCVReturnSuccess;
}
virtual bool initialize(SDL_Window* window, int displayFps) override
{
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
m_DisplayFps = displayFps;
NSScreen* screen = [info.info.cocoa.window screen];
CVReturn status;
if (screen == nullptr) {
// Window not visible on any display, so use a
// CVDisplayLink that can work with all active displays.
// When we become visible, we'll recreate ourselves
// and associate with the new screen.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow is not visible on any display");
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
}
else {
CGDirectDisplayID displayId;
displayId = getDisplayID(screen);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow on display: %x",
displayId);
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
}
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create CVDisplayLink: %d",
status);
return false;
}
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkSetOutputCallback() failed: %d",
status);
return false;
}
status = CVDisplayLinkStart(m_DisplayLink);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkStart() failed: %d",
status);
return false;
}
return true;
}
private:
Pacer* m_Pacer;
CVDisplayLinkRef m_DisplayLink;
int m_DisplayFps;
};
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
return new DisplayLinkVsyncSource(pacer);
}
@@ -0,0 +1,168 @@
#include "dxvsyncsource.h"
// Useful references:
// https://bugs.chromium.org/p/chromium/issues/detail?id=467617
// https://chromium.googlesource.com/chromium/src.git/+/c564f2fe339b2b2abb0c8773c90c83215670ea71/gpu/ipc/service/gpu_vsync_provider_win.cc
DxVsyncSource::DxVsyncSource(Pacer* pacer) :
m_Pacer(pacer),
m_Thread(nullptr),
m_Gdi32Handle(nullptr)
{
SDL_AtomicSet(&m_Stopping, 0);
}
DxVsyncSource::~DxVsyncSource()
{
if (m_Thread != nullptr) {
SDL_AtomicSet(&m_Stopping, 1);
SDL_WaitThread(m_Thread, nullptr);
}
if (m_Gdi32Handle != nullptr) {
FreeLibrary(m_Gdi32Handle);
}
}
bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
{
m_DisplayFps = displayFps;
m_Gdi32Handle = LoadLibraryA("gdi32.dll");
if (m_Gdi32Handle == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to load gdi32.dll: %d",
GetLastError());
return false;
}
m_D3DKMTOpenAdapterFromHdc = (PFND3DKMTOPENADAPTERFROMHDC)GetProcAddress(m_Gdi32Handle, "D3DKMTOpenAdapterFromHdc");
m_D3DKMTCloseAdapter = (PFND3DKMTCLOSEADAPTER)GetProcAddress(m_Gdi32Handle, "D3DKMTCloseAdapter");
m_D3DKMTWaitForVerticalBlankEvent = (PFND3DKMTWAITFORVERTICALBLANKEVENT)GetProcAddress(m_Gdi32Handle, "D3DKMTWaitForVerticalBlankEvent");
if (m_D3DKMTOpenAdapterFromHdc == nullptr ||
m_D3DKMTCloseAdapter == nullptr ||
m_D3DKMTWaitForVerticalBlankEvent == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Missing required function in gdi32.dll");
return false;
}
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
m_Window = info.info.win.window;
m_Thread = SDL_CreateThread(vsyncThread, "DX Vsync Thread", this);
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
SDL_GetError());
return false;
}
return true;
}
int DxVsyncSource::vsyncThread(void* context)
{
DxVsyncSource* me = reinterpret_cast<DxVsyncSource*>(context);
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
HMONITOR lastMonitor = nullptr;
DEVMODEA monitorMode;
monitorMode.dmSize = sizeof(monitorMode);
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
D3DKMT_WAITFORVERTICALBLANKEVENT waitForVblankEventParams;
NTSTATUS status;
// If the monitor has changed from last time, open the new adapter
HMONITOR currentMonitor = MonitorFromWindow(me->m_Window, MONITOR_DEFAULTTONEAREST);
if (currentMonitor != lastMonitor) {
MONITORINFOEXA monitorInfo = {};
monitorInfo.cbSize = sizeof(monitorInfo);
if (!GetMonitorInfoA(currentMonitor, &monitorInfo)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetMonitorInfo() failed: %d",
GetLastError());
SDL_Delay(10);
continue;
}
if (!EnumDisplaySettingsA(monitorInfo.szDevice, ENUM_CURRENT_SETTINGS, &monitorMode)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"EnumDisplaySettings() failed: %d",
GetLastError());
SDL_Delay(10);
continue;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Monitor changed: %s %d Hz",
monitorInfo.szDevice,
monitorMode.dmDisplayFrequency);
if (openAdapterParams.hAdapter != 0) {
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
}
openAdapterParams.hDc = CreateDCA(nullptr, monitorInfo.szDevice, nullptr, nullptr);
if (!openAdapterParams.hDc) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CreateDC() failed: %d",
GetLastError());
SDL_Delay(10);
continue;
}
status = me->m_D3DKMTOpenAdapterFromHdc(&openAdapterParams);
DeleteDC(openAdapterParams.hDc);
if (status != STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DKMTOpenAdapterFromHdc() failed: %x",
status);
SDL_Delay(10);
continue;
}
lastMonitor = currentMonitor;
}
waitForVblankEventParams.hAdapter = openAdapterParams.hAdapter;
waitForVblankEventParams.hDevice = 0;
waitForVblankEventParams.VidPnSourceId = openAdapterParams.VidPnSourceId;
status = me->m_D3DKMTWaitForVerticalBlankEvent(&waitForVblankEventParams);
if (status != STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DKMTWaitForVerticalBlankEvent() failed: %x",
status);
SDL_Delay(10);
continue;
}
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
}
if (openAdapterParams.hAdapter != 0) {
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
}
return 0;
}
@@ -0,0 +1,53 @@
#pragma once
#include "pacer.h"
#include <SDL_syswm.h>
// from <D3dkmthk.h>
typedef LONG NTSTATUS;
typedef UINT D3DKMT_HANDLE;
typedef UINT D3DDDI_VIDEO_PRESENT_SOURCE_ID;
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
#define STATUS_GRAPHICS_PRESENT_OCCLUDED ((NTSTATUS)0xC01E0006L)
typedef struct _D3DKMT_OPENADAPTERFROMHDC {
HDC hDc;
D3DKMT_HANDLE hAdapter;
LUID AdapterLuid;
D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
} D3DKMT_OPENADAPTERFROMHDC;
typedef struct _D3DKMT_CLOSEADAPTER {
D3DKMT_HANDLE hAdapter;
} D3DKMT_CLOSEADAPTER;
typedef struct _D3DKMT_WAITFORVERTICALBLANKEVENT {
D3DKMT_HANDLE hAdapter;
D3DKMT_HANDLE hDevice;
D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
} D3DKMT_WAITFORVERTICALBLANKEVENT;
typedef NTSTATUS(APIENTRY* PFND3DKMTOPENADAPTERFROMHDC)(D3DKMT_OPENADAPTERFROMHDC*);
typedef NTSTATUS(APIENTRY* PFND3DKMTCLOSEADAPTER)(D3DKMT_CLOSEADAPTER*);
typedef NTSTATUS(APIENTRY* PFND3DKMTWAITFORVERTICALBLANKEVENT)(D3DKMT_WAITFORVERTICALBLANKEVENT*);
class DxVsyncSource : public IVsyncSource
{
public:
DxVsyncSource(Pacer* pacer);
virtual ~DxVsyncSource();
virtual bool initialize(SDL_Window* window, int displayFps);
private:
static int vsyncThread(void* context);
Pacer* m_Pacer;
SDL_Thread* m_Thread;
SDL_atomic_t m_Stopping;
HMODULE m_Gdi32Handle;
HWND m_Window;
int m_DisplayFps;
PFND3DKMTOPENADAPTERFROMHDC m_D3DKMTOpenAdapterFromHdc;
PFND3DKMTCLOSEADAPTER m_D3DKMTCloseAdapter;
PFND3DKMTWAITFORVERTICALBLANKEVENT m_D3DKMTWaitForVerticalBlankEvent;
};
@@ -0,0 +1,43 @@
#include "nullthreadedvsyncsource.h"
NullThreadedVsyncSource::NullThreadedVsyncSource(Pacer* pacer) :
m_Pacer(pacer),
m_Thread(nullptr)
{
SDL_AtomicSet(&m_Stopping, 0);
}
NullThreadedVsyncSource::~NullThreadedVsyncSource()
{
if (m_Thread != nullptr) {
SDL_AtomicSet(&m_Stopping, 1);
SDL_WaitThread(m_Thread, nullptr);
}
}
bool NullThreadedVsyncSource::initialize(SDL_Window*, int displayFps)
{
m_DisplayFps = displayFps;
m_Thread = SDL_CreateThread(vsyncThread, "Null Vsync Thread", this);
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
SDL_GetError());
return false;
}
return true;
}
int NullThreadedVsyncSource::vsyncThread(void* context)
{
NullThreadedVsyncSource* me = reinterpret_cast<NullThreadedVsyncSource*>(context);
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
}
return 0;
}
@@ -0,0 +1,21 @@
#pragma once
#include "pacer.h"
class NullThreadedVsyncSource : public IVsyncSource
{
public:
NullThreadedVsyncSource(Pacer* pacer);
virtual ~NullThreadedVsyncSource();
virtual bool initialize(SDL_Window* window, int displayFps);
private:
static int vsyncThread(void* context);
Pacer* m_Pacer;
SDL_Thread* m_Thread;
SDL_atomic_t m_Stopping;
int m_DisplayFps;
};
@@ -0,0 +1,175 @@
#include "pacer.h"
#include "streaming/streamutils.h"
#include "nullthreadedvsyncsource.h"
#ifdef Q_OS_DARWIN
#include "displaylinkvsyncsource.h"
#endif
#ifdef Q_OS_WIN32
#include "dxvsyncsource.h"
#endif
#define FRAME_HISTORY_ENTRIES 8
// We may be woken up slightly late so don't go all the way
// up to the next V-sync since we may accidentally step into
// the next V-sync period. It also takes some amount of time
// to do the render itself, so we can't render right before
// V-sync happens.
#define TIMER_SLACK_MS 3
Pacer::Pacer(IFFmpegRenderer* renderer) :
m_FrameQueueLock(0),
m_VsyncSource(nullptr),
m_VsyncRenderer(renderer),
m_MaxVideoFps(0),
m_DisplayFps(0)
{
}
Pacer::~Pacer()
{
// Stop V-sync callbacks
delete m_VsyncSource;
m_VsyncSource = nullptr;
while (!m_FrameQueue.isEmpty()) {
AVFrame* frame = m_FrameQueue.dequeue();
av_frame_free(&frame);
}
}
// Called in an arbitrary thread by the IVsyncSource on V-sync
// or an event synchronized with V-sync
void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
{
// Make sure initialize() has been called
SDL_assert(m_MaxVideoFps != 0);
SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS);
Uint32 vsyncCallbackStartTime = SDL_GetTicks();
SDL_AtomicLock(&m_FrameQueueLock);
// If the queue length history entries are large, be strict
// about dropping excess frames.
int frameDropTarget = 1;
// If we may get more frames per second than we can display, use
// frame history to drop frames only if consistently above the
// one queued frame mark.
if (m_MaxVideoFps >= m_DisplayFps) {
for (int i = 0; i < m_FrameQueueHistory.count(); i++) {
if (m_FrameQueueHistory[i] <= 1) {
// Be lenient as long as the queue length
// resolves before the end of frame history
frameDropTarget = 3;
break;
}
}
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) {
m_FrameQueueHistory.dequeue();
}
m_FrameQueueHistory.enqueue(m_FrameQueue.count());
}
// Catch up if we're several frames ahead
while (m_FrameQueue.count() > frameDropTarget) {
AVFrame* frame = m_FrameQueue.dequeue();
av_frame_free(&frame);
}
if (m_FrameQueue.isEmpty()) {
SDL_AtomicUnlock(&m_FrameQueueLock);
while (!SDL_TICKS_PASSED(SDL_GetTicks(),
vsyncCallbackStartTime + timeUntilNextVsyncMillis - TIMER_SLACK_MS)) {
SDL_Delay(1);
SDL_AtomicLock(&m_FrameQueueLock);
if (!m_FrameQueue.isEmpty()) {
// Don't release the lock
goto RenderNextFrame;
}
SDL_AtomicUnlock(&m_FrameQueueLock);
}
// Nothing to render at this time
return;
}
RenderNextFrame:
// Grab the first frame
AVFrame* frame = m_FrameQueue.dequeue();
SDL_AtomicUnlock(&m_FrameQueueLock);
// Render it
m_VsyncRenderer->renderFrameAtVsync(frame);
// Free the frame
av_frame_free(&frame);
}
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
{
m_MaxVideoFps = maxVideoFps;
m_EnableVsync = enableVsync;
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
if (m_EnableVsync) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing in tear-free mode: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN)
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
#elif defined(Q_OS_WIN32)
m_VsyncSource = new DxVsyncSource(this);
#else
// Platforms without a VsyncSource will just render frames
// immediately like they used to.
#endif
if (m_VsyncSource != nullptr && !m_VsyncSource->initialize(window, m_DisplayFps)) {
return false;
}
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Minimal latency tearing mode: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps);
}
return true;
}
void Pacer::submitFrame(AVFrame* frame)
{
// Make sure initialize() has been called
SDL_assert(m_MaxVideoFps != 0);
// Queue the frame until the V-sync callback if
// we have a V-sync source, otherwise deliver it
// immediately and hope for the best.
if (isUsingFrameQueue()) {
SDL_AtomicLock(&m_FrameQueueLock);
m_FrameQueue.enqueue(frame);
SDL_AtomicUnlock(&m_FrameQueueLock);
}
else {
m_VsyncRenderer->renderFrameAtVsync(frame);
av_frame_free(&frame);
}
}
bool Pacer::isUsingFrameQueue()
{
return m_VsyncSource != nullptr;
}
@@ -0,0 +1,38 @@
#pragma once
#include "../renderer.h"
#include <QQueue>
class IVsyncSource {
public:
virtual ~IVsyncSource() {}
virtual bool initialize(SDL_Window* window, int displayFps) = 0;
};
class Pacer
{
public:
Pacer(IFFmpegRenderer* renderer);
~Pacer();
void submitFrame(AVFrame* frame);
bool initialize(SDL_Window* window, int maxVideoFps, bool enableVsync);
void vsyncCallback(int timeUntilNextVsyncMillis);
bool isUsingFrameQueue();
private:
QQueue<AVFrame*> m_FrameQueue;
QQueue<int> m_FrameQueueHistory;
SDL_SpinLock m_FrameQueueLock;
IVsyncSource* m_VsyncSource;
IFFmpegRenderer* m_VsyncRenderer;
int m_MaxVideoFps;
int m_DisplayFps;
bool m_EnableVsync;
};
@@ -8,13 +8,24 @@ extern "C" {
class IFFmpegRenderer { class IFFmpegRenderer {
public: public:
enum VSyncConstraint {
VSYNC_FORCE_OFF,
VSYNC_FORCE_ON,
VSYNC_ANY
};
virtual ~IFFmpegRenderer() {} virtual ~IFFmpegRenderer() {}
virtual bool initialize(SDL_Window* window, virtual bool initialize(SDL_Window* window,
int videoFormat, int videoFormat,
int width, int width,
int height) = 0; int height,
int maxFps,
bool enableVsync) = 0;
virtual bool prepareDecoderContext(AVCodecContext* context) = 0; virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
virtual void renderFrame(AVFrame* frame) = 0; virtual void renderFrameAtVsync(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0;
virtual VSyncConstraint getVsyncConstraint() = 0;
}; };
class SdlRenderer : public IFFmpegRenderer { class SdlRenderer : public IFFmpegRenderer {
@@ -24,9 +35,14 @@ public:
virtual bool initialize(SDL_Window* window, virtual bool initialize(SDL_Window* window,
int videoFormat, int videoFormat,
int width, int width,
int height); int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context); virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame); virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private: private:
SDL_Renderer* m_Renderer; SDL_Renderer* m_Renderer;
@@ -1,5 +1,7 @@
#include "renderer.h" #include "renderer.h"
#include <Limelight.h>
SdlRenderer::SdlRenderer() SdlRenderer::SdlRenderer()
: m_Renderer(nullptr), : m_Renderer(nullptr),
m_Texture(nullptr) m_Texture(nullptr)
@@ -21,15 +23,50 @@ SdlRenderer::~SdlRenderer()
bool SdlRenderer::prepareDecoderContext(AVCodecContext*) bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
{ {
/* Nothing to do */ /* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SDL software renderer");
return true; return true;
} }
bool SdlRenderer::needsTestFrame()
{
// This renderer should always work
return false;
}
int SdlRenderer::getDecoderCapabilities()
{
// The FFmpeg CPU decoder can handle reference frame invalidation,
// but only for H.264.
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
}
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
bool SdlRenderer::initialize(SDL_Window* window, bool SdlRenderer::initialize(SDL_Window* window,
int, int,
int width, int width,
int height) int height,
int,
bool enableVsync)
{ {
m_Renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); Uint32 rendererFlags = SDL_RENDERER_ACCELERATED;
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
// In full-screen exclusive mode, we enable V-sync if requested. For other modes, Windows and Mac
// have compositors that make rendering tear-free. Linux compositor varies by distro and user
// configuration but doesn't seem feasible to detect here.
if (enableVsync) {
rendererFlags |= SDL_RENDERER_PRESENTVSYNC;
}
}
m_Renderer = SDL_CreateRenderer(window, -1, rendererFlags);
if (!m_Renderer) { if (!m_Renderer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s", "SDL_CreateRenderer() failed: %s",
@@ -41,6 +78,11 @@ bool SdlRenderer::initialize(SDL_Window* window,
// logical rendering surface size is equal to the stream size // logical rendering surface size is equal to the stream size
SDL_RenderSetLogicalSize(m_Renderer, width, height); SDL_RenderSetLogicalSize(m_Renderer, width, height);
// Draw a black frame until the video stream starts rendering
SDL_SetRenderDrawColor(m_Renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(m_Renderer);
SDL_RenderPresent(m_Renderer);
m_Texture = SDL_CreateTexture(m_Renderer, m_Texture = SDL_CreateTexture(m_Renderer,
SDL_PIXELFORMAT_YV12, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING, SDL_TEXTUREACCESS_STREAMING,
@@ -56,7 +98,7 @@ bool SdlRenderer::initialize(SDL_Window* window,
return true; return true;
} }
void SdlRenderer::renderFrame(AVFrame* frame) void SdlRenderer::renderFrameAtVsync(AVFrame* frame)
{ {
SDL_UpdateYUVTexture(m_Texture, nullptr, SDL_UpdateYUVTexture(m_Texture, nullptr,
frame->data[0], frame->data[0],
@@ -65,6 +107,7 @@ void SdlRenderer::renderFrame(AVFrame* frame)
frame->linesize[1], frame->linesize[1],
frame->data[2], frame->data[2],
frame->linesize[2]); frame->linesize[2]);
SDL_RenderClear(m_Renderer); SDL_RenderClear(m_Renderer);
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr); SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
SDL_RenderPresent(m_Renderer); SDL_RenderPresent(m_Renderer);
@@ -0,0 +1,219 @@
#include "vaapi.h"
#include <streaming/streamutils.h>
#include <SDL_syswm.h>
VAAPIRenderer::VAAPIRenderer()
: m_HwContext(nullptr)
{
}
VAAPIRenderer::~VAAPIRenderer()
{
if (m_HwContext != nullptr) {
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
// Hold onto this VADisplay since we'll need it to uninitialize VAAPI
VADisplay display = vaDeviceContext->display;
av_buffer_unref(&m_HwContext);
if (display) {
vaTerminate(display);
}
}
}
bool
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
{
int err;
SDL_SysWMinfo info;
m_VideoWidth = width;
m_VideoHeight = height;
SDL_GetWindowSize(window, &m_DisplayWidth, &m_DisplayHeight);
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
m_HwContext = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
if (!m_HwContext) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to allocate VAAPI context");
return false;
}
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
m_WindowSystem = info.subsystem;
if (info.subsystem == SDL_SYSWM_X11) {
#ifdef HAVE_LIBVA_X11
m_XWindow = info.info.x11.window;
vaDeviceContext->display = vaGetDisplay(info.info.x11.display);
if (!vaDeviceContext->display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open X11 display for VAAPI");
return false;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI X11 support!");
return false;
#endif
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
#ifdef HAVE_LIBVA_WAYLAND
m_WaylandSurface = info.info.wl.surface;
m_WaylandDisplay = info.info.wl.display;
vaDeviceContext->display = vaGetDisplayWl(info.info.wl.display);
if (!vaDeviceContext->display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open Wayland display for VAAPI");
return false;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI Wayland support!");
return false;
#endif
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VAAPI rendering subsystem: %d",
info.subsystem);
return false;
}
int major, minor;
VAStatus status;
status = vaInitialize(vaDeviceContext->display, &major, &minor);
if (status != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize VAAPI: %d",
status);
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Initialized VAAPI %d.%d",
major, minor);
// This will populate the driver_quirks
err = av_hwdevice_ctx_init(m_HwContext);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize VAAPI context: %d",
err);
return false;
}
// This quirk is set for the VDPAU wrapper which doesn't work with our VAAPI renderer
if (vaDeviceContext->driver_quirks & AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES) {
// Fail and let our VDPAU renderer pick this up
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Avoiding VDPAU wrapper for VAAPI decoding");
return false;
}
return true;
}
bool
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VAAPI accelerated renderer on %s",
m_WindowSystem == SDL_SYSWM_X11 ? "X11" : "Wayland");
return true;
}
bool
VAAPIRenderer::needsTestFrame()
{
// We need a test frame to see if this VAAPI driver
// supports the profile used for streaming
return true;
}
int
VAAPIRenderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
{
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = m_VideoWidth;
src.h = m_VideoHeight;
dst.x = dst.y = 0;
dst.w = m_DisplayWidth;
dst.h = m_DisplayHeight;
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
if (m_WindowSystem == SDL_SYSWM_X11) {
#ifdef HAVE_LIBVA_X11
vaPutSurface(vaDeviceContext->display,
surface,
m_XWindow,
0, 0,
m_VideoWidth, m_VideoHeight,
dst.x, dst.y,
dst.w, dst.h,
NULL, 0, 0);
#endif
}
else if (m_WindowSystem == SDL_SYSWM_WAYLAND) {
#ifdef HAVE_LIBVA_WAYLAND
struct wl_buffer* buffer;
VAStatus status;
status = vaGetSurfaceBufferWl(vaDeviceContext->display,
surface,
VA_FRAME_PICTURE,
&buffer);
if (status == VA_STATUS_SUCCESS) {
wl_surface_attach(m_WaylandSurface, buffer, 0, 0);
wl_surface_damage(m_WaylandSurface, dst.x, dst.y, dst.w, dst.h);
wl_display_flush(m_WaylandDisplay);
wl_surface_commit(m_WaylandSurface);
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaGetSurfaceBufferWl failed(): %d",
status);
}
#endif
}
else {
// We don't accept anything else in initialize().
SDL_assert(false);
}
}
@@ -0,0 +1,62 @@
#pragma once
#include "renderer.h"
// Avoid X11 if SDL was built without it
#ifndef SDL_VIDEO_DRIVER_X11
#warning Unable to use libva-x11 without SDL support
#undef HAVE_LIBVA_X11
#endif
// Avoid Wayland if SDL was built without it
#ifndef SDL_VIDEO_DRIVER_WAYLAND
#warning Unable to use libva-wayland without SDL support
#undef HAVE_LIBVA_WAYLAND
#endif
extern "C" {
#include <va/va.h>
#ifdef HAVE_LIBVA_X11
#include <va/va_x11.h>
#endif
#ifdef HAVE_LIBVA_WAYLAND
#include <va/va_wayland.h>
#endif
#include <libavutil/hwcontext_vaapi.h>
}
class VAAPIRenderer : public IFFmpegRenderer
{
public:
VAAPIRenderer();
virtual ~VAAPIRenderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
int m_WindowSystem;
AVBufferRef* m_HwContext;
#ifdef HAVE_LIBVA_X11
Window m_XWindow;
#endif
#ifdef HAVE_LIBVA_WAYLAND
struct wl_surface* m_WaylandSurface;
struct wl_display* m_WaylandDisplay;
#endif
int m_VideoWidth;
int m_VideoHeight;
int m_DisplayWidth;
int m_DisplayHeight;
};
@@ -0,0 +1,349 @@
#include "vdpau.h"
#include <streaming/streamutils.h>
#include <SDL_syswm.h>
#define BAIL_ON_FAIL(status, something) if ((status) != VDP_STATUS_OK) { \
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, \
#something " failed: %d", (status)); \
return false; \
}
#define GET_PROC_ADDRESS(id, func) status = vdpauCtx->get_proc_address(m_Device, id, (void**)func); \
BAIL_ON_FAIL(status, id)
const VdpRGBAFormat VDPAURenderer::k_OutputFormats[] = {
VDP_RGBA_FORMAT_B8G8R8A8,
VDP_RGBA_FORMAT_R8G8B8A8
};
VDPAURenderer::VDPAURenderer()
: m_HwContext(nullptr),
m_PresentationQueueTarget(0),
m_PresentationQueue(0),
m_VideoMixer(0),
m_NextSurfaceIndex(0)
{
SDL_zero(m_OutputSurface);
}
VDPAURenderer::~VDPAURenderer()
{
if (m_PresentationQueue != 0) {
m_VdpPresentationQueueDestroy(m_PresentationQueue);
}
if (m_VideoMixer != 0) {
m_VdpVideoMixerDestroy(m_VideoMixer);
}
if (m_PresentationQueueTarget != 0) {
m_VdpPresentationQueueTargetDestroy(m_PresentationQueueTarget);
}
for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) {
if (m_OutputSurface[i] != 0) {
m_VdpOutputSurfaceDestroy(m_OutputSurface[i]);
}
}
// This must be done last as it frees VDPAU context required to call
// the functions above.
if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
}
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
{
int err;
VdpStatus status;
SDL_SysWMinfo info;
m_VideoWidth = width;
m_VideoHeight = height;
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VDPAU,
nullptr, nullptr, 0);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create VDPAU context: %d",
err);
return false;
}
AVHWDeviceContext* devCtx = (AVHWDeviceContext*)m_HwContext->data;
AVVDPAUDeviceContext* vdpauCtx = (AVVDPAUDeviceContext*)devCtx->hwctx;
m_Device = vdpauCtx->device;
GET_PROC_ADDRESS(VDP_FUNC_ID_GET_ERROR_STRING, &m_VdpGetErrorString);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_DESTROY, &m_VdpPresentationQueueTargetDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_CREATE, &m_VdpVideoMixerCreate);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_DESTROY, &m_VdpVideoMixerDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_RENDER, &m_VdpVideoMixerRender);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_CREATE, &m_VdpPresentationQueueCreate);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DESTROY, &m_VdpPresentationQueueDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DISPLAY, &m_VdpPresentationQueueDisplay);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_SET_BACKGROUND_COLOR, &m_VdpPresentationQueueSetBackgroundColor);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_BLOCK_UNTIL_SURFACE_IDLE, &m_VdpPresentationQueueBlockUntilSurfaceIdle);
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_CREATE, &m_VdpOutputSurfaceCreate);
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, &m_VdpOutputSurfaceDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, &m_VdpOutputSurfaceQueryCapabilities);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_X11);
if (info.subsystem == SDL_SYSWM_X11) {
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_CREATE_X11,
&m_VdpPresentationQueueTargetCreateX11);
status = m_VdpPresentationQueueTargetCreateX11(m_Device,
info.info.x11.window,
&m_PresentationQueueTarget);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpPresentationQueueTargetCreateX11() failed: %s",
m_VdpGetErrorString(status));
return false;
}
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VDPAU backend does not currently support Wayland");
return false;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VDPAU rendering subsystem: %d",
info.subsystem);
return false;
}
// Try our available output formats to find something the GPU supports
bool foundFormat = false;
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
VdpBool supported;
uint32_t maxWidth, maxHeight;
status = m_VdpOutputSurfaceQueryCapabilities(m_Device, k_OutputFormats[i],
&supported, &maxWidth, &maxHeight);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpOutputSurfaceQueryCapabilities() failed: %s",
m_VdpGetErrorString(status));
return false;
}
if (supported) {
if (m_DisplayWidth <= maxWidth && m_DisplayHeight <= maxHeight) {
m_OutputSurfaceFormat = k_OutputFormats[i];
foundFormat = true;
break;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Display size not within capabilities %dx%d vs %dx%d",
m_DisplayWidth, m_DisplayWidth,
maxWidth, maxHeight);
}
}
}
if (!foundFormat) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No compatible output surface format found!");
return false;
}
// Create the output surfaces
for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) {
// It seems there's some lazy freeing going on or something in VDPAU
// because we can get VDP_STATUS_RESOURCES, then wait a bit and it'll
// complete without a problem.
int tries = 1;
do {
status = m_VdpOutputSurfaceCreate(m_Device, m_OutputSurfaceFormat,
m_DisplayWidth, m_DisplayHeight,
&m_OutputSurface[i]);
if (status != VDP_STATUS_OK) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"VdpOutputSurfaceCreate() try #%d: %s",
tries,
m_VdpGetErrorString(status));
SDL_Delay(250);
}
} while (status == VDP_STATUS_RESOURCES && ++tries <= 10);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpOutputSurfaceCreate() failed: %s",
m_VdpGetErrorString(status));
return false;
}
}
status = m_VdpPresentationQueueCreate(m_Device, m_PresentationQueueTarget,
&m_PresentationQueue);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpPresentationQueueCreate() failed: %s",
m_VdpGetErrorString(status));
return false;
}
// Set the background to opaque black
VdpColor color = {0.0, 0.0, 0.0, 1.0};
m_VdpPresentationQueueSetBackgroundColor(m_PresentationQueue, &color);
return true;
}
bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
// Allow HEVC usage on VDPAU. This was disabled by FFmpeg due to
// GL interop issues, but we use VDPAU for rendering so it's no issue.
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
// This flag is recommended due to hardware underreporting supported levels
context->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VDPAU accelerated renderer");
return true;
}
bool VDPAURenderer::needsTestFrame()
{
// We need a test frame to see if this VDPAU driver
// supports the profile used for streaming
return true;
}
int VDPAURenderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
{
VdpStatus status;
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
// This is safe without locking because this is always called on the main thread
VdpOutputSurface chosenSurface = m_OutputSurface[m_NextSurfaceIndex];
m_NextSurfaceIndex = (m_NextSurfaceIndex + 1) % OUTPUT_SURFACE_COUNT;
// We need to create the mixer on the fly, because we don't know the dimensions
// of our video surfaces in advance of decoding
if (m_VideoMixer == 0) {
VdpChromaType videoSurfaceChroma;
uint32_t videoSurfaceWidth, videoSurfaceHeight;
status = m_VdpVideoSurfaceGetParameters(videoSurface, &videoSurfaceChroma,
&videoSurfaceWidth, &videoSurfaceHeight);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoSurfaceGetParameters() failed: %s",
m_VdpGetErrorString(status));
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"VDPAU surface size: %dx%d",
videoSurfaceWidth, videoSurfaceHeight);
#define PARAM_COUNT 3
const VdpVideoMixerParameter params[PARAM_COUNT] = {
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
VDP_VIDEO_MIXER_PARAMETER_CHROMA_TYPE,
};
const void* const paramValues[PARAM_COUNT] = {
&videoSurfaceWidth,
&videoSurfaceHeight,
&videoSurfaceChroma,
};
status = m_VdpVideoMixerCreate(m_Device, 0, nullptr,
PARAM_COUNT, params, paramValues,
&m_VideoMixer);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoMixerCreate() failed: %s",
m_VdpGetErrorString(status));
return;
}
}
// Wait for this frame to be off the screen
VdpTime pts;
m_VdpPresentationQueueBlockUntilSurfaceIdle(m_PresentationQueue, chosenSurface, &pts);
VdpRect sourceRect, outputRect;
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = m_VideoWidth;
src.h = m_VideoHeight;
dst.x = dst.y = 0;
dst.w = m_DisplayWidth;
dst.h = m_DisplayHeight;
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
outputRect.x0 = dst.x;
outputRect.x1 = dst.x + dst.w;
outputRect.y0 = dst.y;
outputRect.y1 = dst.y + dst.h;
sourceRect.x0 = sourceRect.y0 = 0;
sourceRect.x1 = m_VideoWidth;
sourceRect.y1 = m_VideoHeight;
// Render the next frame into the output surface
status = m_VdpVideoMixerRender(m_VideoMixer,
VDP_INVALID_HANDLE, nullptr,
VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME,
0, nullptr,
videoSurface,
0, nullptr,
&sourceRect,
chosenSurface,
&outputRect,
nullptr,
0,
nullptr);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoMixerRender() failed: %s",
m_VdpGetErrorString(status));
return;
}
// Queue the frame for display immediately
status = m_VdpPresentationQueueDisplay(m_PresentationQueue, chosenSurface, 0, 0, 0);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpPresentationQueueDisplay() failed: %s",
m_VdpGetErrorString(status));
return;
}
}
@@ -0,0 +1,64 @@
#pragma once
#include "renderer.h"
extern "C" {
#include <vdpau/vdpau.h>
#include <vdpau/vdpau_x11.h>
#include <libavutil/hwcontext_vdpau.h>
}
class VDPAURenderer : public IFFmpegRenderer
{
public:
VDPAURenderer();
virtual ~VDPAURenderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
uint32_t m_VideoWidth, m_VideoHeight;
uint32_t m_DisplayWidth, m_DisplayHeight;
AVBufferRef* m_HwContext;
VdpPresentationQueueTarget m_PresentationQueueTarget;
VdpPresentationQueue m_PresentationQueue;
VdpVideoMixer m_VideoMixer;
VdpRGBAFormat m_OutputSurfaceFormat;
VdpDevice m_Device;
#define OUTPUT_SURFACE_COUNT 3
VdpOutputSurface m_OutputSurface[OUTPUT_SURFACE_COUNT];
int m_NextSurfaceIndex;
#define OUTPUT_SURFACE_FORMAT_COUNT 2
static const VdpRGBAFormat k_OutputFormats[OUTPUT_SURFACE_FORMAT_COUNT];
VdpGetErrorString* m_VdpGetErrorString;
VdpPresentationQueueTargetDestroy* m_VdpPresentationQueueTargetDestroy;
VdpVideoMixerCreate* m_VdpVideoMixerCreate;
VdpVideoMixerDestroy* m_VdpVideoMixerDestroy;
VdpVideoMixerRender* m_VdpVideoMixerRender;
VdpPresentationQueueCreate* m_VdpPresentationQueueCreate;
VdpPresentationQueueDestroy* m_VdpPresentationQueueDestroy;
VdpPresentationQueueDisplay* m_VdpPresentationQueueDisplay;
VdpPresentationQueueSetBackgroundColor* m_VdpPresentationQueueSetBackgroundColor;
VdpPresentationQueueBlockUntilSurfaceIdle* m_VdpPresentationQueueBlockUntilSurfaceIdle;
VdpOutputSurfaceCreate* m_VdpOutputSurfaceCreate;
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
// X11 stuff
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
};
+84 -44
View File
@@ -2,11 +2,13 @@
// libavutil both defining AVMediaType // libavutil both defining AVMediaType
#define AVMediaType AVMediaType_FFmpeg #define AVMediaType AVMediaType_FFmpeg
#include "vt.h" #include "vt.h"
#include "pacer/pacer.h"
#undef AVMediaType #undef AVMediaType
#include <SDL_syswm.h> #include <SDL_syswm.h>
#include <Limelight.h> #include <Limelight.h>
#include <mach/mach_time.h>
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <VideoToolbox/VideoToolbox.h> #import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
@@ -20,7 +22,6 @@ public:
m_FormatDesc(nullptr), m_FormatDesc(nullptr),
m_View(nullptr) m_View(nullptr)
{ {
} }
virtual ~VTRenderer() virtual ~VTRenderer()
@@ -32,13 +33,76 @@ public:
if (m_FormatDesc != nullptr) { if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc); CFRelease(m_FormatDesc);
} }
if (m_View != nullptr) {
[m_View removeFromSuperview];
}
} }
// Caller frees frame after we return
virtual void renderFrameAtVsync(AVFrame* frame) override
{
OSStatus status;
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Resetting failed AVSampleBufferDisplay layer");
// Trigger the main thread to recreate the decoder
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
// If the format has changed or doesn't exist yet, construct it with the
// pixel buffer data
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc);
}
status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
pixBuf, &m_FormatDesc);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
status);
return;
}
}
// Queue this sample for the next v-sync
CMSampleTimingInfo timingInfo = {
.duration = kCMTimeInvalid,
.decodeTimeStamp = kCMTimeInvalid,
.presentationTimeStamp = CMTimeMake(mach_absolute_time(), 1000 * 1000 * 1000)
};
CMSampleBufferRef sampleBuffer;
status = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
pixBuf,
m_FormatDesc,
&timingInfo,
&sampleBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
status);
return;
}
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
}
virtual bool initialize(SDL_Window* window, virtual bool initialize(SDL_Window* window,
int videoFormat, int videoFormat,
int, int,
int) override int,
int,
bool) override
{ {
int err; int err;
@@ -119,54 +183,30 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context) override virtual bool prepareDecoderContext(AVCodecContext* context) override
{ {
context->hw_device_ctx = av_buffer_ref(m_HwContext); context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VideoToolbox accelerated renderer");
return true; return true;
} }
virtual void renderFrame(AVFrame* frame) override virtual bool needsTestFrame() override
{ {
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]); // We query VT to determine whether the codec is supported
OSStatus status; return false;
}
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) { virtual int getDecoderCapabilities() override
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, {
"Resetting failed AVSampleBufferDisplay layer"); return 0;
setupDisplayLayer(); }
}
// If the format has changed or doesn't exist yet, construct it with the virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override
// pixel buffer data {
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) { // This renderer is inherently tied to V-sync due how we're
status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, // rendering with AVSampleBufferDisplay layer. Running without
pixBuf, &m_FormatDesc); // the V-Sync source leads to massive stuttering.
if (status != noErr) { return VSYNC_FORCE_ON;
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
status);
return;
}
}
CMSampleBufferRef sampleBuffer;
status = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
pixBuf,
m_FormatDesc,
&kCMTimingInfoInvalid,
&sampleBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
status);
return;
}
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
} }
private: private:
+310 -99
View File
@@ -1,94 +1,69 @@
#include <Limelight.h> #include <Limelight.h>
#include "ffmpeg.h" #include "ffmpeg.h"
#include "streaming/streamutils.h"
#ifdef _WIN32 #ifdef Q_OS_WIN32
#include "ffmpeg-renderers/dxva2.h" #include "ffmpeg-renderers/dxva2.h"
#endif #endif
bool FFmpegVideoDecoder::chooseDecoder( #ifdef Q_OS_DARWIN
StreamingPreferences::VideoDecoderSelection vds, #include "ffmpeg-renderers/vt.h"
SDL_Window* window,
int videoFormat,
int width, int height,
AVCodec*& chosenDecoder,
const AVCodecHWConfig*& chosenHwConfig,
IFFmpegRenderer*& newRenderer)
{
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
chosenDecoder = avcodec_find_decoder(AV_CODEC_ID_H264);
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
chosenDecoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
}
else {
Q_ASSERT(false);
chosenDecoder = nullptr;
}
if (!chosenDecoder) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to find decoder for format: %x",
videoFormat);
return false;
}
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(chosenDecoder, i);
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
// No matching hardware acceleration support.
// This is not an error.
chosenHwConfig = nullptr;
newRenderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
newRenderer->initialize(window, videoFormat, width, height)) {
return true;
}
else {
delete newRenderer;
newRenderer = nullptr;
return false;
}
}
if (!(config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
continue;
}
// Look for acceleration types we support
switch (config->device_type) {
#ifdef _WIN32
case AV_HWDEVICE_TYPE_DXVA2:
newRenderer = new DXVA2Renderer();
break;
#endif #endif
default:
continue;
}
if (newRenderer->initialize(window, videoFormat, width, height)) { #ifdef HAVE_LIBVA
chosenHwConfig = config; #include "ffmpeg-renderers/vaapi.h"
return true; #endif
}
else { #ifdef HAVE_LIBVDPAU
// Failed to initialize #include "ffmpeg-renderers/vdpau.h"
delete newRenderer; #endif
newRenderer = nullptr;
} // This is gross but it allows us to use sizeof()
} #include "ffmpeg_videosamples.cpp"
}
#define FAILED_DECODES_RESET_THRESHOLD 20
bool FFmpegVideoDecoder::isHardwareAccelerated() bool FFmpegVideoDecoder::isHardwareAccelerated()
{ {
return m_HwDecodeCfg != nullptr; return m_HwDecodeCfg != nullptr;
} }
int FFmpegVideoDecoder::getDecoderCapabilities()
{
return m_Renderer->getDecoderCapabilities();
}
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts)
{
FFmpegVideoDecoder* decoder = (FFmpegVideoDecoder*)context->opaque;
const enum AVPixelFormat *p;
for (p = pixFmts; *p != -1; p++) {
// Only match our hardware decoding codec or SW pixel
// format (if not using hardware decoding). It's crucial
// to override the default get_format() which will try
// to gracefully fall back to software decode and break us.
if (*p == (decoder->m_HwDecodeCfg ?
decoder->m_HwDecodeCfg->pix_fmt :
context->pix_fmt)) {
return *p;
}
}
return AV_PIX_FMT_NONE;
}
FFmpegVideoDecoder::FFmpegVideoDecoder() FFmpegVideoDecoder::FFmpegVideoDecoder()
: m_VideoDecoderCtx(nullptr), : m_VideoDecoderCtx(nullptr),
m_DecodeBuffer(1024 * 1024, 0), m_DecodeBuffer(1024 * 1024, 0),
m_HwDecodeCfg(nullptr), m_HwDecodeCfg(nullptr),
m_Renderer(nullptr) m_Renderer(nullptr),
m_ConsecutiveFailedDecodes(0),
m_Pacer(nullptr)
{ {
av_init_packet(&m_Pkt); av_init_packet(&m_Pkt);
SDL_AtomicSet(&m_QueuedFrames, 0);
// Use linear filtering when renderer scaling is required // Use linear filtering when renderer scaling is required
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
@@ -96,29 +71,74 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
FFmpegVideoDecoder::~FFmpegVideoDecoder() FFmpegVideoDecoder::~FFmpegVideoDecoder()
{ {
avcodec_close(m_VideoDecoderCtx); reset();
av_free(m_VideoDecoderCtx); }
m_VideoDecoderCtx = nullptr;
m_HwDecodeCfg = nullptr; IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
{
return m_Renderer;
}
void FFmpegVideoDecoder::reset()
{
// Drop any frames still queued to ensure
// they are properly freed.
SDL_Event event;
while (SDL_AtomicGet(&m_QueuedFrames) > 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Waiting for %d frames to return",
SDL_AtomicGet(&m_QueuedFrames));
if (SDL_PeepEvents(&event,
1,
SDL_GETEVENT,
SDL_USEREVENT,
SDL_USEREVENT) == 1) {
dropFrame(&event.user);
}
else {
SDL_Delay(10);
SDL_PumpEvents();
}
}
delete m_Pacer;
m_Pacer = nullptr;
// This must be called after deleting Pacer because it
// may be holding AVFrames to free in its destructor.
// However, it must be called before deleting the IFFmpegRenderer
// since the codec context may be referencing objects that we
// need to delete in the renderer destructor.
avcodec_free_context(&m_VideoDecoderCtx);
delete m_Renderer; delete m_Renderer;
m_Renderer = nullptr; m_Renderer = nullptr;
} }
bool FFmpegVideoDecoder::initialize( bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds, int videoFormat, int width, int height,
SDL_Window* window, int maxFps, bool enableVsync, bool testOnly)
int videoFormat,
int width,
int height,
int)
{ {
AVCodec* decoder; auto vsyncConstraint = m_Renderer->getVsyncConstraint();
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully disabled by the active renderer");
enableVsync = false;
}
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) {
// FIXME: This duplicates logic in Session.cpp
int displayHz = StreamUtils::getDisplayRefreshRate(window);
if (displayHz + 5 >= maxFps) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully enabled by the active renderer");
enableVsync = true;
}
}
if (!chooseDecoder(vds, window, videoFormat, width, height, m_Pacer = new Pacer(m_Renderer);
decoder, m_HwDecodeCfg, m_Renderer)) { if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
// Error logged in chooseDecoder()
return false; return false;
} }
@@ -132,6 +152,10 @@ bool FFmpegVideoDecoder::initialize(
// Always request low delay decoding // Always request low delay decoding
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY; m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
// Allow display of corrupt frames and frames missing references
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
m_VideoDecoderCtx->flags2 |= AV_CODEC_FLAG2_SHOW_ALL;
// Enable slice multi-threading for software decoding // Enable slice multi-threading for software decoding
if (!m_HwDecodeCfg) { if (!m_HwDecodeCfg) {
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE; m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
@@ -146,12 +170,20 @@ bool FFmpegVideoDecoder::initialize(
m_VideoDecoderCtx->width = width; m_VideoDecoderCtx->width = width;
m_VideoDecoderCtx->height = height; m_VideoDecoderCtx->height = height;
m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR
m_VideoDecoderCtx->get_format = ffGetFormat;
// Allow the renderer to attach data to this decoder // Allow the renderer to attach data to this decoder
if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) { if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) {
return false; return false;
} }
// Nobody must override our ffGetFormat
SDL_assert(m_VideoDecoderCtx->get_format == ffGetFormat);
// Stash a pointer to this object in the context
SDL_assert(m_VideoDecoderCtx->opaque == nullptr);
m_VideoDecoderCtx->opaque = this;
int err = avcodec_open2(m_VideoDecoderCtx, decoder, nullptr); int err = avcodec_open2(m_VideoDecoderCtx, decoder, nullptr);
if (err < 0) { if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -160,9 +192,155 @@ bool FFmpegVideoDecoder::initialize(
return false; return false;
} }
// FFMpeg doesn't completely initialize the codec until the codec
// config data comes in. This would be too late for us to change
// our minds on the selected video codec, so we'll do a trial run
// now to see if things will actually work when the video stream
// comes in.
if (testOnly) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
m_Pkt.data = (uint8_t*)k_H264TestFrame;
m_Pkt.size = sizeof(k_H264TestFrame);
}
else {
m_Pkt.data = (uint8_t*)k_HEVCTestFrame;
m_Pkt.size = sizeof(k_HEVCTestFrame);
}
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) {
char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring));
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Test decode failed: %s", errorstring);
return false;
}
}
#ifdef QT_DEBUG
// Restore default log level before streaming
av_log_set_level(AV_LOG_INFO);
#endif
return true; return true;
} }
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
{
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
return nullptr;
}
// Look for acceleration types we support
switch (hwDecodeCfg->device_type) {
#ifdef Q_OS_WIN32
case AV_HWDEVICE_TYPE_DXVA2:
return new DXVA2Renderer();
#endif
#ifdef Q_OS_DARWIN
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
return VTRendererFactory::createRenderer();
#endif
#ifdef HAVE_LIBVA
case AV_HWDEVICE_TYPE_VAAPI:
return new VAAPIRenderer();
#endif
#ifdef HAVE_LIBVDPAU
case AV_HWDEVICE_TYPE_VDPAU:
return new VDPAURenderer();
#endif
default:
return nullptr;
}
}
bool FFmpegVideoDecoder::initialize(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync)
{
AVCodec* decoder;
#ifdef QT_DEBUG
// Increase log level during initialization
av_log_set_level(AV_LOG_DEBUG);
#endif
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
decoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
}
else {
Q_ASSERT(false);
decoder = nullptr;
}
if (!decoder) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to find decoder for format: %x",
videoFormat);
return false;
}
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
// No matching hardware acceleration support.
// This is not an error.
m_HwDecodeCfg = nullptr;
m_Renderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
return true;
}
else {
reset();
return false;
}
}
m_Renderer = createAcceleratedRenderer(config);
if (!m_Renderer) {
continue;
}
m_HwDecodeCfg = config;
// Initialize the hardware codec and submit a test frame if the renderer needs it
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, m_Renderer->needsTestFrame())) {
if (m_Renderer->needsTestFrame()) {
// The test worked, so now let's initialize it for real
reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
return true;
}
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"Decoder failed to initialize after successful test");
reset();
}
}
else {
// No test required. Good to go now.
return true;
}
}
else {
// Failed to initialize or test frame failed, so keep looking
reset();
}
}
}
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du) int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
{ {
PLENTRY entry = du->bufferList; PLENTRY entry = du->bufferList;
@@ -188,12 +366,23 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt); err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) { if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Decoding failed: %d", err);
char errorstring[512]; char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring)); av_strerror(err, errorstring, sizeof(errorstring));
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Decoding failed: %s", errorstring); "avcodec_send_packet() failed: %s", errorstring);
// If we've failed a bunch of decodes in a row, the decoder/renderer is
// clearly unhealthy, so let's generate a synthetic reset event to trigger
// the event loop to destroy and recreate the decoder.
if (++m_ConsecutiveFailedDecodes == FAILED_DECODES_RESET_THRESHOLD) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Resetting decoder due to consistent failure");
SDL_Event event;
event.type = SDL_RENDER_DEVICE_RESET;
SDL_PushEvent(&event);
}
return DR_NEED_IDR; return DR_NEED_IDR;
} }
@@ -208,17 +397,29 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
err = avcodec_receive_frame(m_VideoDecoderCtx, frame); err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
if (err == 0) { if (err == 0) {
SDL_Event event; // Reset failed decodes count if we reached this far
m_ConsecutiveFailedDecodes = 0;
event.type = SDL_USEREVENT; // Queue the frame for rendering from the main thread
event.user.code = SDL_CODE_FRAME_READY; SDL_AtomicIncRef(&m_QueuedFrames);
event.user.data1 = frame; queueFrame(frame);
// The main thread will handle freeing this
SDL_PushEvent(&event);
} }
else { else {
av_frame_free(&frame); av_frame_free(&frame);
char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring));
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"avcodec_receive_frame() failed: %s", errorstring);
if (++m_ConsecutiveFailedDecodes == FAILED_DECODES_RESET_THRESHOLD) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Resetting decoder due to consistent failure");
SDL_Event event;
event.type = SDL_RENDER_DEVICE_RESET;
SDL_PushEvent(&event);
}
} }
return DR_OK; return DR_OK;
@@ -228,13 +429,23 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event) void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
{ {
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1); AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
m_Renderer->renderFrame(frame); m_Pacer->submitFrame(frame);
av_frame_free(&frame); SDL_AtomicDecRef(&m_QueuedFrames);
} }
// Called on main thread // Called on main thread
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event) void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
{ {
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1); AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
av_frame_free(&frame); // If Pacer is using a frame queue, we can just queue the
// frame and let it decide whether to drop or not. If Pacer
// is submitting directly for rendering, we drop the frame
// ourselves.
if (m_Pacer->isUsingFrameQueue()) {
m_Pacer->submitFrame(frame);
}
else {
av_frame_free(&frame);
}
SDL_AtomicDecRef(&m_QueuedFrames);
} }
+23 -10
View File
@@ -2,6 +2,7 @@
#include "decoder.h" #include "decoder.h"
#include "ffmpeg-renderers/renderer.h" #include "ffmpeg-renderers/renderer.h"
#include "ffmpeg-renderers/pacer/pacer.h"
extern "C" { extern "C" {
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
@@ -16,26 +17,38 @@ public:
int videoFormat, int videoFormat,
int width, int width,
int height, int height,
int frameRate) override; int maxFps,
bool enableVsync) override;
virtual bool isHardwareAccelerated() override; virtual bool isHardwareAccelerated() override;
virtual int getDecoderCapabilities() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override; virtual int submitDecodeUnit(PDECODE_UNIT du) override;
virtual void renderFrame(SDL_UserEvent* event) override; virtual void renderFrame(SDL_UserEvent* event) override;
virtual void dropFrame(SDL_UserEvent* event) override; virtual void dropFrame(SDL_UserEvent* event) override;
virtual IFFmpegRenderer* getRenderer();
private: private:
bool bool completeInitialization(AVCodec* decoder, SDL_Window* window,
chooseDecoder( int videoFormat, int width, int height,
StreamingPreferences::VideoDecoderSelection vds, int maxFps, bool enableVsync, bool testOnly);
SDL_Window* window,
int videoFormat, IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
int width, int height,
AVCodec*& chosenDecoder, void reset();
const AVCodecHWConfig*& chosenHwConfig,
IFFmpegRenderer*& newRenderer); static
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts);
AVPacket m_Pkt; AVPacket m_Pkt;
AVCodecContext* m_VideoDecoderCtx; AVCodecContext* m_VideoDecoderCtx;
QByteArray m_DecodeBuffer; QByteArray m_DecodeBuffer;
const AVCodecHWConfig* m_HwDecodeCfg; const AVCodecHWConfig* m_HwDecodeCfg;
IFFmpegRenderer* m_Renderer; IFFmpegRenderer* m_Renderer;
SDL_atomic_t m_QueuedFrames;
int m_ConsecutiveFailedDecodes;
Pacer* m_Pacer;
static const uint8_t k_H264TestFrame[];
static const uint8_t k_HEVCTestFrame[];
}; };
@@ -0,0 +1,19 @@
#include "ffmpeg.h"
// 720p 60 FPS H.264 with 1 reference frame
const uint8_t FFmpegVideoDecoder::k_H264TestFrame[] = {
0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x20, 0xac, 0x2b, 0x40, 0x28, 0x02, 0xdd, 0x80, 0xb5, 0x06, 0x06, 0x06, 0xa5, 0x00, 0x00, 0x03, 0x03, 0xe8, 0x00, 0x01, 0xd4, 0xc0, 0x8f, 0x4a, 0xa0,
0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x3c, 0xb0,
0x00, 0x00, 0x00, 0x01, 0x65, 0xb8, 0x02, 0x01, 0x67, 0x25, 0x1b, 0xf4, 0xfa, 0x7d, 0x40, 0x1a, 0x78, 0xe5, 0x10, 0x52, 0xc2, 0xee, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0xc6, 0xef, 0xbb, 0x81, 0x85, 0x2d, 0x47, 0xda, 0xca, 0x4c, 0x00, 0x00, 0x03, 0x00, 0x02, 0x7b, 0xcf, 0x80, 0x00, 0x45, 0x40, 0x01, 0x8d, 0xa6, 0x00, 0x01, 0x64, 0x00, 0x0e, 0x03, 0xc8, 0x00, 0x0e, 0x10, 0x00, 0xbd, 0xc5, 0x00, 0x01, 0x11, 0x00, 0x0e, 0xa3, 0x80, 0x00, 0x38, 0xa0,
0x00, 0x00, 0x01, 0x65, 0x00, 0x6e, 0x2e, 0x00, 0x83, 0x7f, 0xb4, 0x8e, 0x79, 0xa5, 0xff, 0x84, 0x3f, 0x7f, 0x34, 0x3f, 0x97, 0x1b, 0xaf, 0x31, 0x5f, 0x6e, 0xaa, 0xb6, 0xac, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x78, 0x36, 0x9d, 0xa4, 0x65, 0xf9, 0x1e, 0x5b, 0x3a, 0xb0, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x41, 0x80, 0x00, 0xc5, 0xc8, 0x00, 0x00, 0xfa, 0x00, 0x03, 0x24, 0x00, 0x0e, 0x20, 0x00, 0x3f, 0x80, 0x01, 0x32, 0x00, 0x08, 0x68, 0x00, 0x3e, 0xc0, 0x03, 0x8a,
0x00, 0x00, 0x01, 0x65, 0x00, 0x37, 0x0b, 0x80, 0x21, 0x7f, 0xe3, 0x85, 0x1c, 0xd9, 0xff, 0xe1, 0x1b, 0x01, 0xfa, 0xc0, 0x3e, 0x11, 0x7e, 0xfe, 0x45, 0x5c, 0x43, 0x69, 0x31, 0x4b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x24, 0xae, 0x1a, 0xbb, 0xae, 0x75, 0x9e, 0x35, 0xae, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x03, 0x64, 0x00, 0x09, 0x98, 0x00, 0x1e, 0xc0, 0x00, 0x64, 0x80, 0x01, 0xc4, 0x00, 0x07, 0xf0, 0x00, 0x42, 0xf0, 0x00, 0x00, 0xe1, 0x98, 0x00, 0x05, 0x4b, 0x28, 0x00, 0x06, 0x04,
0x00, 0x00, 0x01, 0x65, 0x00, 0x14, 0xa2, 0xe0, 0x08, 0x5f, 0xe3, 0x85, 0x1c, 0xd9, 0xff, 0xe1, 0x1b, 0x01, 0xfa, 0xc0, 0x3e, 0x11, 0x7e, 0xfe, 0x45, 0x5c, 0x43, 0x69, 0x31, 0x4b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x24, 0xb9, 0x7d, 0xb4, 0x70, 0x4d, 0x15, 0xc5, 0x0a, 0x4e, 0x60, 0x00, 0x00, 0x03, 0x00, 0x00, 0x26, 0xa8, 0xb0, 0x00, 0x13, 0xcd, 0xcc, 0x00, 0x07, 0x48, 0x88, 0x00, 0x06, 0x29, 0x69, 0x00, 0x01, 0x16, 0xac, 0x80, 0x00, 0x9e, 0x4e, 0x80, 0x00, 0x3f, 0x84, 0x20, 0x00, 0x6f, 0x41, 0xa0, 0x00, 0x20, 0x00, 0x02, 0x16, 0xb8, 0x00, 0x08, 0x08
};
// 720p 60 FPS HEVC with 1 reference frame
const uint8_t FFmpegVideoDecoder::k_HEVCTestFrame[] = {
0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xac, 0x09,
0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xa0, 0x02, 0x80, 0x80, 0x2e, 0x1f, 0x13, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3, 0x01, 0x6a, 0x0c, 0x0c, 0x0d, 0x48, 0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00, 0x07, 0x85, 0xf1, 0xa2, 0xd0,
0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
0x00, 0x00, 0x00, 0x01, 0x2a, 0x01, 0xac, 0x0c, 0xf0, 0xe6, 0xf6, 0x5e, 0xff, 0xd9, 0x97, 0xeb, 0x44, 0x7d, 0x1f, 0xa4, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x03, 0x52, 0x7c, 0x4e, 0x5b, 0xfe, 0xbe, 0xc2, 0x87, 0x20, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x2b, 0x60,
};
+112
View File
@@ -0,0 +1,112 @@
#include "sl.h"
SLVideoDecoder::SLVideoDecoder()
: m_VideoContext(nullptr),
m_VideoStream(nullptr)
{
}
SLVideoDecoder::~SLVideoDecoder()
{
if (m_VideoStream != nullptr) {
SLVideo_FreeStream(m_VideoStream);
}
if (m_VideoContext != nullptr) {
SLVideo_FreeContext(m_VideoContext);
}
}
bool
SLVideoDecoder::isHardwareAccelerated()
{
// SLVideo is always hardware accelerated
return true;
}
int
SLVideoDecoder::getDecoderCapabilities()
{
return 0;
}
bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*,
int videoFormat, int, int, int frameRate, bool)
{
// SLVideo only supports hardware decoding
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
return false;
}
// SLVideo only supports H.264
if (videoFormat != VIDEO_FORMAT_H264) {
return false;
}
m_VideoContext = SLVideo_CreateContext();
if (!m_VideoContext) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_CreateContext() failed");
return false;
}
// Create a low latency H.264 stream
m_VideoStream = SLVideo_CreateStream(m_VideoContext, k_ESLVideoFormatH264, 1);
if (!m_VideoStream) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_CreateStream() failed");
return false;
}
SLVideo_SetStreamTargetFramerate(m_VideoStream, frameRate, 1);
return true;
}
int
SLVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
{
int err;
err = SLVideo_BeginFrame(m_VideoStream, du->fullLength);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_BeginFrame() failed: %d",
err);
// Need an IDR frame to resync
return DR_NEED_IDR;
}
PLENTRY entry = du->bufferList;
while (entry != nullptr) {
err = SLVideo_WriteFrameData(m_VideoStream,
entry->data,
entry->length);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_WriteFrameData() failed: %d",
err);
// Need an IDR frame to resync
return DR_NEED_IDR;
}
entry = entry->next;
}
err = SLVideo_SubmitFrame(m_VideoStream);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_SubmitFrame() failed: %d",
err);
// Need an IDR frame to resync
return DR_NEED_IDR;
}
return DR_OK;
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "decoder.h"
#include <SLVideo.h>
class SLVideoDecoder : public IVideoDecoder
{
public:
SLVideoDecoder();
virtual ~SLVideoDecoder();
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int frameRate,
bool enableVsync);
virtual bool isHardwareAccelerated();
virtual int getDecoderCapabilities();
virtual int submitDecodeUnit(PDECODE_UNIT du);
// Unused since rendering is done directly from the decode thread
virtual void renderFrame(SDL_UserEvent*) {}
virtual void dropFrame(SDL_UserEvent*) {}
private:
CSLVideoContext* m_VideoContext;
CSLVideoStream* m_VideoStream;
};
+1 -1
View File
@@ -1,5 +1,5 @@
#pragma once #pragma once
#define THROW_BAD_ALLOC_IF_NULL(x) \ #define THROW_BAD_ALLOC_IF_NULL(x) \
if ((x) == nullptr) throw new std::bad_alloc() if ((x) == nullptr) throw std::bad_alloc()
+3
View File
@@ -0,0 +1,3 @@
SOURCES = main.cpp
LIBS += -lSLVideo
+7
View File
@@ -0,0 +1,7 @@
#include <SLVideo.h>
int main()
{
SLVideo_CreateContext();
return 0;
}
+1
View File
@@ -0,0 +1 @@
Versions/Current/Headers
+1
View File
@@ -0,0 +1 @@
Versions/Current/Resources
+1
View File
@@ -0,0 +1 @@
Versions/Current/SDL2
Loaded 100 of 271 files, more files were not shown because too many files have changed in this diff. Show more