Compare commits

..
182 Commits
Author SHA1 Message Date
Cameron Gutman b6872456cd Version 0.9.1 2019-02-17 13:13:57 -08:00
Cameron Gutman 2a12b018da Fix special key combos on non-QWERTY latin keyboards 2019-02-17 13:07:54 -08:00
Cameron Gutman f64486c88f Version 0.9.0 2019-02-17 00:18:42 -08:00
Cameron Gutman 80a40ac5f9 Increase frame drop threshold 2019-02-16 23:13:35 -08:00
Cameron Gutman a1956350b1 Rename pacing drop to jitter drop to be less confusing for users 2019-02-16 23:04:25 -08:00
Cameron Gutman 2c09875556 Fix input handler cleanup on startup failure 2019-02-16 01:39:31 -08:00
Cameron Gutman 561eed4cbd Use 2 concurrent jobs on Travis CI 2019-02-16 00:59:00 -08:00
Cameron Gutman a8f72df7f8 Fix in-tree builds 2019-02-16 00:42:09 -08:00
Cameron Gutman ab4127f940 Use presence of a frame after rendering rather than render time for frame dropping 2019-02-15 22:44:52 -08:00
Cameron Gutman f03d12b677 Use a separate render thread even with a V-Sync source 2019-02-15 22:09:50 -08:00
Cameron Gutman 81c6202582 Rename renderFrameAtVsync() to renderFrame() in preparation for further refactoring 2019-02-15 21:07:02 -08:00
Cameron Gutman 6cab2bec61 Don't trust VT to tell us whether decode is supported 2019-02-15 19:10:21 -08:00
Cameron Gutman 94ffda653c Update SDL_GameControllerDB to add new mappings 2019-02-15 19:00:30 -08:00
Cameron Gutman c2268b45f6 Merge pull request #177 from konst2/master
Fix hotkeys for non-latin keyboard layouts
2019-02-15 18:57:24 -08:00
Cameron Gutman 38b8a59fe5 Adjust overlay color for more contrast 2019-02-15 18:53:40 -08:00
Cameron Gutman 640e3213ee Copy the required data files into place for building in the IDE 2019-02-15 18:47:42 -08:00
Cameron Gutman b1799009b3 Use the Path class to lookup ModeSeven.ttf 2019-02-15 18:13:36 -08:00
Cameron Gutman f8e693a060 Drop samples to account for lost time reinitializing audio. Fixes #176 2019-02-14 22:32:54 -08:00
Cameron Gutman aba806bc32 Implement debug overlay for VideoToolbox renderer 2019-02-13 20:55:52 -08:00
Cameron Gutman b1fb3f2391 Set FFmpeg log level at debug until first frame is rendered 2019-02-13 18:34:59 -08:00
konst2 041e901c61 Fix hotkeys for non-latin keyboard layouts 2019-02-13 19:56:44 +03:00
Cameron Gutman bb72134992 Attempt to fix Travis CI Linux build 2019-02-12 23:25:23 -08:00
Cameron Gutman f83853c2fb OverlayManager now manages font size for the overlays 2019-02-12 22:36:56 -08:00
Cameron Gutman d5f7b20be3 Update common-c with sign extension fix 2019-02-12 22:24:47 -08:00
Cameron Gutman b878d64460 Check rumble controller number is within the supported range 2019-02-12 22:05:55 -08:00
Cameron Gutman 5397e8ae1a Don't use SDL_GameControllerGetPlayerIndex() because the indexes may be non-contiguous (and wrong) on XInput 2019-02-12 22:04:05 -08:00
Cameron Gutman 46bd8ad83d Add debug overlay for SDL renderer 2019-02-12 21:55:15 -08:00
Cameron Gutman 4dac57cf8c Add SDL2_ttf dependency for Linux 2019-02-12 21:38:51 -08:00
Cameron Gutman 95cb026446 Add SDL2_ttf for macOS 2019-02-12 21:32:11 -08:00
Cameron Gutman 76ea714257 Add SDL2_ttf for Windows 2019-02-12 21:18:55 -08:00
Cameron Gutman 5788fa7204 Make OverlayManager own the overlay color setting 2019-02-12 20:39:10 -08:00
Cameron Gutman d077c6450f Split SdlRenderer from renderer.h 2019-02-12 19:58:36 -08:00
Cameron Gutman 4499ed3878 Work around dialog rendering issue with QGnomePlatform. Fixes #161 2019-02-12 19:45:14 -08:00
Cameron Gutman feb5e3bc78 Fix SDL_WasInit asserts firing due to lifetime management issues with SdlInputHandler 2019-02-12 19:30:02 -08:00
Cameron Gutman 23f1060756 Polish the resume/quit buttons 2019-02-12 19:07:48 -08:00
Cameron Gutman 6ed512e762 Provide a renderer callback when the overlay data changes 2019-02-12 18:43:38 -08:00
Cameron Gutman 947970e07b Shortcut decoder initialization when only using it for capability testing 2019-02-12 18:42:53 -08:00
Cameron Gutman df567b405a Minor bugfixes to haptics 2019-02-11 21:55:56 -08:00
MINICOM\mrb11 7b0596d9df Add controller rumble support. 2019-02-11 21:39:55 -08:00
Cameron Gutman 4b7200ae69 Don't display the connection terminated dialog for intended disconnects 2019-02-10 02:19:37 -08:00
Cameron Gutman 710648f3f4 Fix QuitSegue -> StreamSegue transition to be direct via replacement and disallow toolbar interactions 2019-02-09 21:15:17 -08:00
Cameron Gutman d2002b6a62 Fix SOPS issues causing 720p60 settings on non-standard resolutions and FPS values 2019-02-09 20:37:11 -08:00
Cameron Gutman 8de42b3199 Fix gamepad menu focus and dismissal issues 2019-02-09 19:59:01 -08:00
Cameron Gutman 48e78d2cd3 Don't allow focus on invisible menu items 2019-02-09 19:21:43 -08:00
Cameron Gutman 1fd099db70 Log when StretchRect() fails 2019-02-09 18:47:57 -08:00
Cameron Gutman 2986a40c5a Fix some gamepads working for UI navigation but not in game 2019-01-29 22:01:05 -08:00
Cameron Gutman 8bb6d4d0d9 Fix VDPAU scaling on AMD cards. Fixes #171 2019-01-29 20:52:41 -08:00
Cameron Gutman 00f08578ee Update common-c to fix IDR issue on GFE 3.17 2019-01-29 20:18:03 -08:00
Cameron Gutman 6a514c7f51 Update readme to point to Snap package 2019-01-29 18:30:56 -08:00
Cameron Gutman 21aad643f3 Use 32 pixel alignment for HEVC to fix Intel green line issue 2019-01-28 21:26:27 -08:00
Cameron Gutman 8687448966 Use StretchRect() on Intel GPUs or if VideoProcessBlt() fails 2019-01-28 19:18:43 -08:00
Cameron Gutman 6fcc8b721b Add a button for quitting the running app 2019-01-26 23:57:02 -08:00
Cameron Gutman b4bbd58c77 Add long press action on PC view for touchscreens 2019-01-26 23:11:09 -08:00
Cameron Gutman 1a45b8f07a Fix dialog box styles 2019-01-26 21:45:32 -08:00
Cameron Gutman 4471a858db Fix miscounted pacer dropped frames in non-pacing mode 2019-01-22 21:21:29 -08:00
Cameron Gutman 22e712af5a Add v-sync text to stats overlay 2019-01-22 20:55:58 -08:00
Cameron Gutman 9799b82d53 Improve long render time logic 2019-01-22 20:55:35 -08:00
Cameron Gutman 78915e0b99 Use a separate render thread when not using frame pacing 2019-01-22 20:31:31 -08:00
Cameron Gutman c9cc65aae7 Fix appstream metadata release date for v0.8.1 2019-01-22 19:07:14 -08:00
Cameron Gutman a4edd9d202 Submit frame directly from the decoder thread to avoid interference from the main thread 2019-01-21 20:27:37 -08:00
Cameron Gutman 7747624c80 Revert "Wake up main thread immediately after new frame is pushed"
This reverts commit aab7191a44.
2019-01-21 20:10:13 -08:00
Cameron Gutman aab7191a44 Wake up main thread immediately after new frame is pushed 2019-01-21 18:57:14 -08:00
Cameron Gutman 1f972b1828 Require several consecutive long render times to drop a frame 2019-01-21 17:58:42 -08:00
Cameron Gutman 58b77cf589 Fix inaccurate pacer drop and network drop percentages and add total FPS stat 2019-01-21 17:43:15 -08:00
Cameron Gutman c064f87889 Drop a frame if the rendering time exceeds the v-sync period 2019-01-20 19:59:29 -08:00
Cameron Gutman d6b4c8e7da Avoid blocking decoding operations during Present calls 2019-01-20 14:08:53 -08:00
Cameron Gutman 49f0b45fa8 Update build requirements for Windows 2019-01-20 11:58:30 -08:00
Cameron Gutman b19c4bfc5d Add performance overlay for DXVA2 renderer 2019-01-19 23:05:56 -08:00
Cameron Gutman e944c819d9 Add prefix for NvLogLevel enum values 2019-01-19 21:32:35 -08:00
Cameron Gutman 75f631599c Ensure all polling threads are dead before destroying hosts, even ones that have been detached by stopPollingAsync() 2019-01-19 19:18:50 -08:00
Cameron Gutman e0bebeae25 Allow theme overrides via environment variables 2019-01-19 17:06:35 -08:00
Cameron Gutman 32e7ddf272 Version 0.8.1 2019-01-08 00:48:49 -08:00
Cameron Gutman 7091e0324e Update macOS builds to FFmpeg 4.1 2019-01-07 21:43:18 -08:00
Cameron Gutman 8f8532921c Update FFmpeg build script for Mac to avoid manual steps 2019-01-07 21:39:59 -08:00
Cameron Gutman e9936992ec Update Windows builds to FFmpeg 4.1 2019-01-07 21:08:08 -08:00
Cameron Gutman 6375055b3e Set scripts executable 2019-01-07 20:34:56 -08:00
Cameron Gutman 88b1ea296c Remove video bitrate tooltip since audio is accounted for during remote streaming 2019-01-06 15:41:00 -08:00
Cameron Gutman 2a13a869f3 Don't allow the scroll wheel to move the bitrate slider 2019-01-06 15:39:54 -08:00
Cameron Gutman 89c342bb6f Introduce more granular HTTP timeouts 2019-01-06 14:35:33 -08:00
Cameron Gutman 067f5e33aa Increase audio buffer on Linux to 25 ms. Fixes #147. Fixes #157. 2019-01-06 13:49:20 -08:00
Cameron Gutman 33695f1c6b Update common-c to reduce audio latency 2019-01-06 13:29:06 -08:00
Cameron Gutman 52ef84873e Remove excess latency control logic from SDL renderer, since it doesn't appear to work very well anyway 2019-01-06 13:28:43 -08:00
Cameron Gutman c21ba5c808 Allow the audio renderer to be overriden by the ML_AUDIO environment variable 2019-01-06 13:28:05 -08:00
Cameron Gutman 6cf408f12b Update macOS build steps 2019-01-05 19:23:01 -08:00
Cameron Gutman 8bdc2d285d Rename macOS build output to match final release name 2019-01-05 19:06:00 -08:00
Cameron Gutman a66fbcd6be Use version.txt for Info.plist too 2019-01-05 18:53:17 -08:00
Cameron Gutman 93402a62fc Archive symbol ZIP to ML_SYMBOL_ARCHIVE folder 2019-01-05 18:20:53 -08:00
Cameron Gutman 2a56fb6c70 Add config.test generated files to gitignore 2019-01-05 18:10:20 -08:00
Cameron Gutman 8276c031e9 Name the Windows output binaries to match their final names on GitHub 2019-01-05 18:09:52 -08:00
Cameron Gutman 102e5f6de9 Move the version string into version.txt 2019-01-05 18:08:34 -08:00
Cameron Gutman 1f7bdbfbcb Fail signed release builds if unstaged changes exist 2019-01-05 17:25:40 -08:00
Cameron Gutman f2fdd048c7 Use stun.moonlight-stream.org for STUN 2019-01-04 20:16:27 -08:00
Cameron Gutman 5a2cfc15a6 Ensure WoL works properly on multi-homed hosts. Fixes #135 2019-01-04 20:05:31 -08:00
Cameron Gutman 11c7131875 Replace forgotten clamp to frameCountMax removed by e75dce2543 2019-01-03 18:49:18 -08:00
Cameron Gutman 34aacc90d7 Include version string in update tooltip 2019-01-03 18:00:54 -08:00
Cameron Gutman e75dce2543 Remove one-sample-per-callback write logic to fix crackling at 44.1 KHz. Fixes #152 2019-01-03 17:23:29 -08:00
Cameron Gutman e2774309fb Fix context menus on Qt 5.9 2019-01-02 19:49:06 -08:00
Cameron Gutman dfefdc93c5 Update macOS icns 2019-01-01 19:33:08 -08:00
Cameron Gutman d46ea28bba Merge pull request #151 from Jorys-Paulin/feature/icon-tweaks
Updated Moonlight icon
2019-01-01 19:32:21 -08:00
Cameron Gutman d35c5bcaba Print socket errors when sending WoL packets 2019-01-01 19:20:09 -08:00
Cameron Gutman 2b14856d7f Update common-c for strict remote bitrate logic 2019-01-01 19:03:52 -08:00
Jorys Paulin 3316fdb80e Updated Moonlight icon 2018-12-26 13:29:49 +01:00
Cameron Gutman c5d70c48c9 Fix QMap usage where insertion on read of missing value was not intended 2018-12-26 00:59:30 -08:00
Cameron Gutman 82230ef2f5 Version 0.8.0 2018-12-25 22:29:27 -08:00
Cameron Gutman dcfd3d3e48 Add a debug print for V-sync 2018-12-25 22:19:23 -08:00
Cameron Gutman cb1e2ac32e Avoid filling our client logs with QNetworkReplyHttpImplPrivate warnings 2018-12-25 21:54:20 -08:00
Cameron Gutman ec2c15946a Update audio buffering parameters based on local testing 2018-12-25 21:45:15 -08:00
Cameron Gutman 8be73142f8 Fix cert pinning on macOS 2018-12-25 21:15:59 -08:00
Cameron Gutman d1640e3bb8 Audio latency tweaks and fix for PulseAudio with A2DP 2018-12-25 17:54:18 -08:00
Cameron Gutman 8612e6726d Remove accidentally enabled debug prints 2018-12-25 14:05:55 -08:00
Cameron Gutman 3b4694e9f8 Add libraries required by libsoundio 2018-12-25 14:05:35 -08:00
Cameron Gutman 14cb2e1999 Remove latency drop logic and replace by single frame writes 2018-12-25 14:05:17 -08:00
Cameron Gutman cfabaf334e Add frame pacing toggle 2018-12-25 12:57:00 -08:00
Cameron Gutman c054536fab Count Pacer delay in frame stats 2018-12-25 12:09:45 -08:00
Cameron Gutman 35aac18b4e Improve bitrate slider label and add tooltip 2018-12-25 11:48:34 -08:00
Cameron Gutman de3928cb4d Reduce FPs on the unmapped gamepad dialog 2018-12-25 11:41:48 -08:00
Cameron Gutman 9bf612b800 Update common-c with bitrate fix 2018-12-24 20:10:37 -08:00
Cameron Gutman d9a8d67e3d Use a shared UID for all Moonlight clients 2018-12-22 21:04:44 -08:00
Cameron Gutman 99b0a56a6a Add additional cert pinning assert 2018-12-22 20:05:45 -08:00
Cameron Gutman 61d7aa0400 Require cert pinning for HTTPS 2018-12-22 19:55:28 -08:00
Cameron Gutman 1e98594972 Handle server cert change by pairing again 2018-12-21 19:17:32 -08:00
Cameron Gutman c6383f042c Pin server cert to host during pairing 2018-12-21 18:08:07 -08:00
Cameron Gutman b4d8e0e551 Update bug_report.md 2018-12-19 23:11:15 +04:00
Cameron Gutman d696536486 Version 0.7.0 2018-12-05 22:49:42 -08:00
Cameron Gutman b46a3cf759 Show stream error dialog when launching from the CLI 2018-12-05 22:22:30 -08:00
Cameron Gutman 8a5e3cdb76 Fix returning to app page when start fails 2018-12-05 22:17:26 -08:00
Cameron Gutman 4fb39226ef Fix 'moonlight -v' output on Linux 2018-12-05 22:01:22 -08:00
Cameron Gutman c40174034c Don't kill the app on the host when the session terminates unexpectedly 2018-12-05 20:17:00 -08:00
Cameron Gutman 5029c855f0 Simplify app quitting implementation a bit 2018-12-05 19:49:06 -08:00
Cameron Gutman 2eee839eaa Update moonlight-common-c 2018-12-05 18:47:56 -08:00
Cameron Gutman f42c7fec1e Update SDL GC DB 2018-12-05 18:47:44 -08:00
Janne Hakonen 0ab07303c9 Add quit cli command and app quit option after stream session. Fixes #92 (#138)
* Add quit cli command and app quit option after stream session. Fixes #92

* Code review fixes.
2018-12-05 18:45:28 -08:00
Cameron Gutman ad47990a87 Make update icon more obnoxious so people actually update 2018-12-05 18:40:21 -08:00
Cameron Gutman 1968461ce9 Allow HTTP redirects when checking for updates 2018-12-03 23:29:49 -08:00
Cameron Gutman 8aa3b5d3da Switch to new hostname 2018-12-03 21:48:49 -08:00
Cameron Gutman 23e77eefb0 Rename multi-controller checkbox to be more descriptive 2018-11-29 20:10:47 -08:00
Cameron Gutman 094443cf26 Rename start in windowed mode option to be more accurate 2018-11-28 21:46:14 -08:00
Cameron Gutman b97758ef46 Switch to libsoundio on Linux now that PA works reliably after d1fe752028 2018-11-28 00:13:04 -08:00
Cameron Gutman d1fe752028 Fix busy looping in libsoundio audio renderer when no audio is playing 2018-11-27 23:43:31 -08:00
Cameron Gutman b26df787cd Update to upstream libsoundio 2018-11-27 23:26:15 -08:00
Cameron Gutman 4c5b254730 Add larger downloads section 2018-11-23 02:09:54 -08:00
Cameron Gutman 9a153b7170 Remove unneccessary hardcoded text color for better style support 2018-11-22 02:35:25 -08:00
Cameron Gutman 065a6caee5 Use automatic remote streaming detection 2018-11-21 20:55:25 -08:00
Cameron Gutman 9c7df02941 Avoid duplicate framework copy now that it's handle in the project file 2018-11-20 22:43:10 -08:00
Cameron Gutman c63c5100e2 Version 0.6.4 2018-11-20 22:23:03 -08:00
Cameron Gutman bf05088d70 Remove checks that rely on display modes because they can be inaccurate on machines when the monitor is off 2018-11-20 22:02:43 -08:00
Cameron Gutman e7bb146f92 Allow streaming to match the refresh rate in unsupported mode 2018-11-20 21:47:08 -08:00
Cameron Gutman 688c4a90d9 Use the raw display bounds not the usable display bounds for full-screen calculations 2018-11-18 23:10:30 -08:00
Cameron Gutman 165ad96be1 Fix stack overflow if SDL gamepad initialization fails 2018-11-18 22:04:44 -08:00
Cameron Gutman 115ced7ab4 Only precompile QML files on release builds 2018-11-18 22:04:11 -08:00
Cameron Gutman ef15b5641a Fix accidental dependency on system SDL2.framework when building in the IDE 2018-11-18 21:23:46 -08:00
Cameron Gutman be50464619 Revert "Update SDL2.framework to 2.0.9"
This reverts commit 7254224347.
2018-11-18 16:04:52 -08:00
Cameron Gutman d847b71c86 Fix host virtual gamepads being reconnected during stream startup and causing issues with multiple controllers. Fixes #108 2018-11-18 12:05:17 -08:00
Cameron Gutman dfcc1ff899 Also print gamepad mapping on gamepad attach 2018-11-17 17:23:20 -08:00
Cameron Gutman 65562937f3 Print gamepad GUID string in logs 2018-11-17 17:10:52 -08:00
Cameron Gutman 7b5dc33a21 Version 0.6.3 2018-11-16 01:17:57 -08:00
Cameron Gutman f64c13b9ec Ujpdate common-c with NAT64 fixes 2018-11-16 00:40:42 -08:00
Cameron Gutman 25f70e570e Add timer-based input batching for GFE 3.16 2018-11-15 20:52:07 -08:00
Cameron Gutman b27b64c25c Initialize the SDL timer subsystem 2018-11-15 20:45:32 -08:00
Cameron Gutman bfc64c926a Reorganize settings page now that vertical scrolling works 2018-11-15 16:41:02 -08:00
Cameron Gutman 88ad983aa4 Update common-c for GFE 3.16 2018-11-15 13:11:01 -08:00
Cameron Gutman a95d2e76f5 Fix settings page scrolling. Fixes #106 2018-11-13 18:03:53 -08:00
Cameron Gutman 037886ba5b Add tooltips and update help text 2018-11-04 14:36:12 -08:00
Cameron Gutman 011edfe2a0 Disable any usage of proxy servers 2018-11-04 14:15:53 -08:00
Cameron Gutman 1a5a6773ce Add option to determine if Moonlight launches in full-screen. Fixes #103 2018-11-04 13:57:33 -08:00
Cameron Gutman dda22fd387 Rename mouse acceleration toggle to raw mouse input 2018-11-04 13:31:28 -08:00
Cameron Gutman 7254224347 Update SDL2.framework to 2.0.9 2018-11-03 14:18:17 -07:00
Cameron Gutman 789e52af57 Use SDL_GameControllerGetPlayerIndex() as a hint for picking player indexes 2018-11-03 13:04:34 -07:00
Cameron Gutman b2fcd6c084 Update Windows SDL headers to 2.0.9 2018-11-03 13:01:35 -07:00
Cameron Gutman 76e5794203 Remove SetThreadExecutionState() call now that SDL_DisableScreenSaver() works on Windows 2018-11-03 12:44:14 -07:00
Cameron Gutman a81332dd23 Update SDL2 for Windows to MSVC build of 5dc13016cf34b2c503a7599fabdd4f62d1811fdb 2018-11-03 12:25:38 -07:00
Cameron Gutman e1fab22d98 Fix accidental removal of decoder deletion in previous commit 2018-10-31 18:49:37 -07:00
Cameron Gutman cb5e58d465 Fix use-after-free caught by debug build of SDL2 2018-10-31 18:20:39 -07:00
Cameron Gutman eaeb9ec6f7 Version 0.6.2 2018-10-28 19:57:38 -07:00
Cameron Gutman 88bb17fca8 Change Manufacturer string to "Moonlight Game Streaming Project" 2018-10-28 18:11:07 -07:00
Cameron Gutman f672b8534f Change quit tip based on whether gamepads are attached 2018-10-28 17:59:07 -07:00
Cameron Gutman 839c0a45a0 Avoid polling server status while streaming 2018-10-28 17:53:49 -07:00
Cameron Gutman 974e44ba4c Update common-c for more unique STUN error codes 2018-10-28 17:20:34 -07:00
Cameron Gutman 9461ebec2d Log an error if STUN fails 2018-10-26 20:54:17 -07:00
Cameron Gutman 48d8a53cd6 Resolve WAN address using STUN if the PC is discovered over mDNS 2018-10-26 20:44:51 -07:00
Cameron Gutman 2a8dfd63da Update common-c to include STUN code 2018-10-26 20:22:38 -07:00
Cameron Gutman 49ac431792 Fix calling convention of LoadLibrary hooks which caused x86 builds to crash 2018-10-15 07:41:50 -07:00
232 changed files with 29127 additions and 1079 deletions

No files matched your search

+10 -2
View File
@@ -13,6 +13,9 @@ Any special steps that are required for the bug to appear.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots. If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
**Affected games**
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
**Moonlight settings (please complete the following information)** **Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults? - Have any settings been adjusted from defaults?
- If so, which settings have been changed? - If so, which settings have been changed?
@@ -22,10 +25,15 @@ If applicable, add screenshots to help explain your problem. If the issue is rel
- OS: [e.g. Windows 10 1803] - OS: [e.g. Windows 10 1803]
- Moonlight Version: [e.g. v0.2.0] - Moonlight Version: [e.g. v0.2.0]
**Logs (please attach)** **Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1803]
- GeForce Experience version: [e.g. 3.16.0.140]
- Nvidia GPU driver: [e.g. 417.35]
**Moonlight 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 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 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` - On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
**Additional context** **Additional context**
Add any other context about the problem here. Anything else you think may be relevant to the issue
+2
View File
@@ -3,3 +3,5 @@
**/.vs/ **/.vs/
build/ build/
config.tests/*/.qmake.stash
config.tests/*/Makefile
+1 -1
View File
@@ -9,7 +9,7 @@
url = https://github.com/gabomdq/SDL_GameControllerDB.git url = https://github.com/gabomdq/SDL_GameControllerDB.git
[submodule "soundio/libsoundio"] [submodule "soundio/libsoundio"]
path = soundio/libsoundio path = soundio/libsoundio
url = https://github.com/cgutman/libsoundio.git url = https://github.com/andrewrk/libsoundio.git
[submodule "h264bitstream/h264bitstream"] [submodule "h264bitstream/h264bitstream"]
path = h264bitstream/h264bitstream path = h264bitstream/h264bitstream
url = https://github.com/aizvorski/h264bitstream.git url = https://github.com/aizvorski/h264bitstream.git
+8 -5
View File
@@ -3,6 +3,9 @@ language: cpp
git: git:
depth: 1 depth: 1
env:
- NJOBS=2
matrix: matrix:
include: include:
- os: osx - os: osx
@@ -14,13 +17,12 @@ matrix:
addons: addons:
apt: apt:
sources: sources:
- sourceline: 'ppa:beineri/opt-qt596-xenial' - sourceline: 'ppa:beineri/opt-qt597-xenial'
- sourceline: 'ppa:jonathonf/ffmpeg-4' - sourceline: 'ppa:jonathonf/ffmpeg-4'
packages: packages:
- qt59base - qt59base
- qt59quickcontrols2 - qt59quickcontrols2
- qt59svg - qt59svg
- qt59multimedia
- libesd0-dev - libesd0-dev
- libgl-dev - libgl-dev
- libegl1-mesa-dev - libegl1-mesa-dev
@@ -35,7 +37,8 @@ matrix:
install: install:
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5' - '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force 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 - if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.9 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
before_script: before_script:
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh' - '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
@@ -46,5 +49,5 @@ before_script:
script: script:
- qmake moonlight-qt.pro -spec $QMAKESPEC - qmake moonlight-qt.pro -spec $QMAKESPEC
- make debug - make -j$NJOBS debug
- make release - make -j$NJOBS release
+4 -4
View File
@@ -84,7 +84,7 @@ private:
return false; return false;
} }
static HMODULE LoadLibraryAHook(LPCSTR lpLibFileName) static HMODULE WINAPI LoadLibraryAHook(LPCSTR lpLibFileName)
{ {
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) { if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY); SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
@@ -94,7 +94,7 @@ private:
return s_RealLoadLibraryA(lpLibFileName); return s_RealLoadLibraryA(lpLibFileName);
} }
static HMODULE LoadLibraryWHook(LPCWSTR lpLibFileName) static HMODULE WINAPI LoadLibraryWHook(LPCWSTR lpLibFileName)
{ {
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) { if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY); SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
@@ -104,7 +104,7 @@ private:
return s_RealLoadLibraryW(lpLibFileName); return s_RealLoadLibraryW(lpLibFileName);
} }
static HMODULE LoadLibraryExAHook(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags) static HMODULE WINAPI LoadLibraryExAHook(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
{ {
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) { if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY); SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
@@ -114,7 +114,7 @@ private:
return s_RealLoadLibraryExA(lpLibFileName, hFile, dwFlags); return s_RealLoadLibraryExA(lpLibFileName, hFile, dwFlags);
} }
static HMODULE LoadLibraryExWHook(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags) static HMODULE WINAPI LoadLibraryExWHook(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
{ {
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) { if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY); SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
+17 -11
View File
@@ -1,11 +1,9 @@
# 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. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC. [Moonlight PC](https://moonlight-stream.org) 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).
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). 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) [![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)
@@ -20,39 +18,47 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
- Keyboard and mouse support - Keyboard and mouse support
- Gamepad support with SDL gamepad mappings - Gamepad support with SDL gamepad mappings
## Downloads
- [Windows and macOS](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
## Building ## Building
### General Build Requirements ### General Build Requirements
* Qt 5.9 SDK or later * Qt 5.9 SDK or later
### Windows-specific Requirements ### Windows-specific Requirements
* Windows 7 or later * Windows 7 or later
* Visual Studio 2015 or later * [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2015 or later (Community edition is fine)
* Select MSVC Desktop toolchain during Qt installation * Select MSVC Desktop toolchain during Qt installation
* 7-Zip (only if building installers for non-development PCs) * [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
* WiX Toolset v3.11 or later (only if building installers for non-development PCs) * [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
### Mac-specific Requirements ### Mac-specific Requirements
* macOS El Capitan (10.11) or later * macOS Sierra (10.12) or later
* Xcode with High Sierra (10.13) SDK (Mojave SDK not yet supported until Qt 5.12 later this year) * Xcode 10
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs) * [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
### Linux-specific Requirements ### Linux-specific Requirements
* GCC or Clang * GCC or Clang
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel` * Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/ * FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
### Build Setup Steps ### Build Setup Steps
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
* You may also use Homebrew on macOS or your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later. * You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
2. Run `git submodule update --init --recursive` from within `moonlight-qt/` 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. 3. Open the project in Qt Creator or build from qmake on the command line.
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder. * To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`. * For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`. * For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
## Contribute ## Contribute
1. Fork us 1. Fork us
2. Write code 2. Write code
3. Send Pull Requests 3. Send Pull Requests
Check out our [website](http://moonlight-stream.com) for project links and information. Check out our [website](https://moonlight-stream.org) for project links and information.
+2 -2
View File
@@ -23,9 +23,9 @@
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
<true/> <true/>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.6.1</string> <string>VERSION</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.6.1</string> <string>VERSION</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Moonlight</string> <string>Moonlight</string>
</dict> </dict>
BIN
View File
Binary file not shown.
+60 -15
View File
@@ -18,8 +18,10 @@ CONFIG += force_debug_info
# Since this binds the app against the Qt runtime version, we will only # Since this binds the app against the Qt runtime version, we will only
# do this for Windows and Mac, since they ship with the Qt runtime. # do this for Windows and Mac, since they ship with the Qt runtime.
win32|macx { win32|macx {
CONFIG(release, debug|release) {
CONFIG += qtquickcompiler CONFIG += qtquickcompiler
} }
}
TEMPLATE = app TEMPLATE = app
@@ -35,25 +37,31 @@ DEFINES += QT_DEPRECATED_WARNINGS
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
win32 { win32 {
INCLUDEPATH += $$PWD/../libs/windows/include INCLUDEPATH += \
$$PWD/../libs/windows/include \
$$(DXSDK_DIR)/Include
contains(QT_ARCH, i386) { contains(QT_ARCH, i386) {
LIBS += -L$$PWD/../libs/windows/lib/x86 LIBS += -L$$PWD/../libs/windows/lib/x86
LIBS += -L$$(DXSDK_DIR)/Lib/x86
} }
contains(QT_ARCH, x86_64) { contains(QT_ARCH, x86_64) {
LIBS += -L$$PWD/../libs/windows/lib/x64 LIBS += -L$$PWD/../libs/windows/lib/x64
LIBS += -L$$(DXSDK_DIR)/Lib/x64
} }
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.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 $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers INCLUDEPATH += $$PWD/../libs/mac/include
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
} }
unix:!macx { unix:!macx {
CONFIG += link_pkgconfig CONFIG += link_pkgconfig soundio
PKGCONFIG += openssl sdl2 opus PKGCONFIG += openssl sdl2 SDL2_ttf opus
# For libsoundio # For libsoundio
packagesExist(libpulse) { packagesExist(libpulse) {
@@ -83,11 +91,11 @@ unix:!macx {
} }
} }
win32 { win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
CONFIG += ffmpeg soundio CONFIG += ffmpeg soundio
} }
macx { macx {
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
# For libsoundio # For libsoundio
@@ -98,6 +106,7 @@ macx {
SOURCES += \ SOURCES += \
main.cpp \ main.cpp \
backend/computerseeker.cpp \
backend/identitymanager.cpp \ backend/identitymanager.cpp \
backend/nvcomputer.cpp \ backend/nvcomputer.cpp \
backend/nvhttp.cpp \ backend/nvhttp.cpp \
@@ -105,6 +114,7 @@ SOURCES += \
backend/computermanager.cpp \ backend/computermanager.cpp \
backend/boxartmanager.cpp \ backend/boxartmanager.cpp \
cli/commandlineparser.cpp \ cli/commandlineparser.cpp \
cli/quitstream.cpp \
cli/startstream.cpp \ cli/startstream.cpp \
settings/streamingpreferences.cpp \ settings/streamingpreferences.cpp \
streaming/input.cpp \ streaming/input.cpp \
@@ -117,10 +127,12 @@ SOURCES += \
backend/autoupdatechecker.cpp \ backend/autoupdatechecker.cpp \
path.cpp \ path.cpp \
settings/mappingmanager.cpp \ settings/mappingmanager.cpp \
gui/sdlgamepadkeynavigation.cpp gui/sdlgamepadkeynavigation.cpp \
streaming/video/overlaymanager.cpp
HEADERS += \ HEADERS += \
utils.h \ utils.h \
backend/computerseeker.h \
backend/identitymanager.h \ backend/identitymanager.h \
backend/nvcomputer.h \ backend/nvcomputer.h \
backend/nvhttp.h \ backend/nvhttp.h \
@@ -128,6 +140,7 @@ HEADERS += \
backend/computermanager.h \ backend/computermanager.h \
backend/boxartmanager.h \ backend/boxartmanager.h \
cli/commandlineparser.h \ cli/commandlineparser.h \
cli/quitstream.h \
cli/startstream.h \ cli/startstream.h \
settings/streamingpreferences.h \ settings/streamingpreferences.h \
streaming/input.h \ streaming/input.h \
@@ -141,7 +154,8 @@ HEADERS += \
backend/autoupdatechecker.h \ backend/autoupdatechecker.h \
path.h \ path.h \
settings/mappingmanager.h \ settings/mappingmanager.h \
gui/sdlgamepadkeynavigation.h gui/sdlgamepadkeynavigation.h \
streaming/video/overlaymanager.h
# Platform-specific renderers and decoders # Platform-specific renderers and decoders
ffmpeg { ffmpeg {
@@ -154,10 +168,10 @@ ffmpeg {
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \ streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
HEADERS += \ HEADERS += \
streaming/video/ffmpeg.h \ streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h \ streaming/video/ffmpeg-renderers/renderer.h \
streaming/video/ffmpeg-renderers/sdlvid.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h \ streaming/video/ffmpeg-renderers/pacer/pacer.h \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
} }
@@ -271,6 +285,18 @@ else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debu
INCLUDEPATH += $$PWD/../AntiHooking INCLUDEPATH += $$PWD/../AntiHooking
DEPENDPATH += $$PWD/../AntiHooking DEPENDPATH += $$PWD/../AntiHooking
defineTest(copyToDestDir) {
files = $$1
for(FILE, files) {
!equals(PWD, $$OUT_PWD) {
QMAKE_POST_LINK += $$QMAKE_COPY $$shell_quote($$shell_path($$PWD/$$FILE)) $$shell_quote($$shell_path($$OUT_PWD)) $$escape_expand(\\n\\t)
}
}
export(QMAKE_POST_LINK)
}
unix:!macx: { unix:!macx: {
isEmpty(PREFIX) { isEmpty(PREFIX) {
PREFIX = /usr/local PREFIX = /usr/local
@@ -293,9 +319,15 @@ unix:!macx: {
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appstream.path = $$PREFIX/$$DATADIR/metainfo/ appstream.path = $$PREFIX/$$DATADIR/metainfo/
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/" appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
CONFIG(debug, debug|release) {
# Copy the required data files into the application directory only for debug builds.
# Release builds will use the INSTALLS variable to place these in the correct folders.
copyToDestDir(SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf)
}
INSTALLS += target appdata desktop icons appstream INSTALLS += target appdata desktop icons appstream
} }
win32 { win32 {
@@ -304,16 +336,29 @@ win32 {
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
QMAKE_TARGET_PRODUCT = Moonlight QMAKE_TARGET_PRODUCT = Moonlight
# Copy the required data files into the application directory
copyToDestDir(SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf)
CONFIG -= embed_manifest_exe CONFIG -= embed_manifest_exe
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
} }
macx { macx {
QMAKE_INFO_PLIST = $$PWD/Info.plist # Create Info.plist in object dir with the correct version string
system(cp $$PWD/Info.plist $$OUT_PWD/Info.plist)
system(sed -i -e 's/VERSION/$$cat(version.txt)/g' $$OUT_PWD/Info.plist)
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf
APP_BUNDLE_RESOURCES.path = Contents/Resources APP_BUNDLE_RESOURCES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true)
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
QMAKE_RPATHDIR += @executable_path/../Frameworks
} }
VERSION = 0.6.1 VERSION = "$$cat(version.txt)"
DEFINES += VERSION_STR=\\\"0.6.1\\\" DEFINES += VERSION_STR=\\\"$$cat(version.txt)\\\"
+6 -2
View File
@@ -12,6 +12,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
// Never communicate over HTTP // Never communicate over HTTP
m_Nam.setStrictTransportSecurityEnabled(true); m_Nam.setStrictTransportSecurityEnabled(true);
// Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)), connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*))); this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
@@ -27,7 +30,7 @@ void AutoUpdateChecker::start()
{ {
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update #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 // We'll get a callback when this is finished
QUrl url("https://moonlight-stream.com/updates/qt.json"); QUrl url("https://moonlight-stream.org/updates/qt.json");
m_Nam.get(QNetworkRequest(url)); m_Nam.get(QNetworkRequest(url));
#endif #endif
} }
@@ -113,7 +116,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
if (currentVer < latestVer) { if (currentVer < latestVer) {
qDebug() << "Update available"; qDebug() << "Update available";
emit onUpdateAvailable(updateObj["browser_url"].toString()); emit onUpdateAvailable(updateObj["version"].toString(),
updateObj["browser_url"].toString());
return; return;
} }
else if (currentVer > latestVer) { else if (currentVer > latestVer) {
+1 -1
View File
@@ -12,7 +12,7 @@ public:
Q_INVOKABLE void start(); Q_INVOKABLE void start();
signals: signals:
void onUpdateAvailable(QString url); void onUpdateAvailable(QString newVersion, QString url);
private slots: private slots:
void handleUpdateCheckRequestFinished(QNetworkReply* reply); void handleUpdateCheckRequestFinished(QNetworkReply* reply);
+1 -1
View File
@@ -97,7 +97,7 @@ void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QU
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId) QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
{ {
NvHTTP http(computer->activeAddress); NvHTTP http(computer->activeAddress, computer->serverCert);
QString cachePath = getFilePathForBoxArt(computer, appId); QString cachePath = getFilePathForBoxArt(computer, appId);
QImage image; QImage image;
+89 -77
View File
@@ -2,6 +2,9 @@
#include "nvhttp.h" #include "nvhttp.h"
#include "settings/streamingpreferences.h" #include "settings/streamingpreferences.h"
#include <Limelight.h>
#include <QtEndian>
#include <QThread> #include <QThread>
#include <QThreadPool> #include <QThreadPool>
@@ -24,16 +27,16 @@ public:
private: private:
bool tryPollComputer(QString address, bool& changed) bool tryPollComputer(QString address, bool& changed)
{ {
NvHTTP http(address); NvHTTP http(address, m_Computer->serverCert);
QString serverInfo; QString serverInfo;
try { try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE); serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE);
} catch (...) { } catch (...) {
return false; return false;
} }
NvComputer newState(address, serverInfo); NvComputer newState(address, serverInfo, QSslCertificate());
// Ensure the machine that responded is the one we intended to contact // Ensure the machine that responded is the one we intended to contact
if (m_Computer->uuid != newState.uuid) { if (m_Computer->uuid != newState.uuid) {
@@ -49,7 +52,7 @@ private:
{ {
Q_ASSERT(m_Computer->activeAddress != nullptr); Q_ASSERT(m_Computer->activeAddress != nullptr);
NvHTTP http(m_Computer->activeAddress); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
QVector<NvApp> appList; QVector<NvApp> appList;
@@ -171,38 +174,19 @@ ComputerManager::~ComputerManager()
delete m_MdnsBrowser; delete m_MdnsBrowser;
m_MdnsBrowser = nullptr; m_MdnsBrowser = nullptr;
{ // Interrupt polling
QMutableMapIterator<QString, QThread*> i(m_PollThreads); for (ComputerPollingEntry* entry : m_PollEntries) {
entry->interrupt();
// Interrupt all polling threads
while (i.hasNext()) {
i.next();
i.value()->requestInterruption();
} }
// Wait for all threads to terminate before destroying // Delete all polling entries (and associated threads)
// the NvComputer objects. for (ComputerPollingEntry* entry : m_PollEntries) {
i.toFront(); delete entry;
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
thread->wait();
delete thread;
i.remove();
}
} }
{ // Destroy all NvComputer objects now that polling is halted
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts); for (NvComputer* computer : m_KnownHosts) {
delete computer;
// Destroy all NvComputer objects
while (i.hasNext()) {
i.next();
delete i.value();
i.remove();
}
} }
} }
@@ -215,7 +199,7 @@ void ComputerManager::saveHosts()
settings.beginWriteArray(SER_HOSTS); settings.beginWriteArray(SER_HOSTS);
for (int i = 0; i < m_KnownHosts.count(); i++) { for (int i = 0; i < m_KnownHosts.count(); i++) {
settings.setArrayIndex(i); settings.setArrayIndex(i);
m_KnownHosts[m_KnownHosts.keys()[i]]->serialize(settings); m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
} }
settings.endArray(); settings.endArray();
} }
@@ -262,17 +246,23 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
return; return;
} }
if (m_PollThreads.contains(computer->uuid)) { ComputerPollingEntry* pollingEntry;
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
return; if (!m_PollEntries.contains(computer->uuid)) {
pollingEntry = m_PollEntries[computer->uuid] = new ComputerPollingEntry();
}
else {
pollingEntry = m_PollEntries[computer->uuid];
} }
if (!pollingEntry->isActive()) {
PcMonitorThread* thread = new PcMonitorThread(computer); PcMonitorThread* thread = new PcMonitorThread(computer);
connect(thread, SIGNAL(computerStateChanged(NvComputer*)), connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
this, SLOT(handleComputerStateChanged(NvComputer*))); this, SLOT(handleComputerStateChanged(NvComputer*)));
m_PollThreads[computer->uuid] = thread; pollingEntry->setActiveThread(thread);
thread->start(); thread->start();
} }
}
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer, void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
const QHostAddress& address) const QHostAddress& address)
@@ -314,34 +304,23 @@ public:
void run() void run()
{ {
QThread* pollingThread; ComputerPollingEntry* pollingEntry;
// Only do the minimum amount of work while holding the writer lock. // Only do the minimum amount of work while holding the writer lock.
// We must release it before calling saveHosts(). // We must release it before calling saveHosts().
{ {
QWriteLocker lock(&m_ComputerManager->m_Lock); QWriteLocker lock(&m_ComputerManager->m_Lock);
pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid]; pollingEntry = m_ComputerManager->m_PollEntries.take(m_Computer->uuid);
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid); m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
} }
// Persist the new host list // Persist the new host list
m_ComputerManager->saveHosts(); m_ComputerManager->saveHosts();
// Delete the polling thread // Delete the polling entry first. This will stop all polling threads too.
if (pollingThread != nullptr) { delete pollingEntry;
pollingThread->requestInterruption();
// We must wait here because we're going to delete computer
// and we can't do that out from underneath the poller.
pollingThread->wait();
// The thread is safe to delete now
Q_ASSERT(pollingThread->isFinished());
delete pollingThread;
}
// Finally, delete the computer itself. This must be done // Finally, delete the computer itself. This must be done
// last because the polling thread might be using it. // last because the polling thread might be using it.
@@ -382,7 +361,7 @@ private:
NvPairingManager pairingManager(m_Computer->activeAddress); NvPairingManager pairingManager(m_Computer->activeAddress);
try { try {
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin); NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
switch (result) switch (result)
{ {
case NvPairingManager::PairState::PIN_WRONG: case NvPairingManager::PairState::PIN_WRONG:
@@ -435,7 +414,7 @@ signals:
private: private:
void run() void run()
{ {
NvHTTP http(m_Computer->activeAddress); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
try { try {
if (m_Computer->currentGameId != 0) { if (m_Computer->currentGameId != 0) {
@@ -496,20 +475,8 @@ void ComputerManager::stopPollingAsync()
m_MdnsBrowser = nullptr; m_MdnsBrowser = nullptr;
// Interrupt all threads, but don't wait for them to terminate // Interrupt all threads, but don't wait for them to terminate
QMutableMapIterator<QString, QThread*> i(m_PollThreads); for (ComputerPollingEntry* entry : m_PollEntries) {
while (i.hasNext()) { entry->interrupt();
i.next();
QThread* thread = i.value();
// Let this thread delete itself when it's finished
connect(thread, SIGNAL(finished()),
thread, SLOT(deleteLater()));
// The threads will delete themselves when they terminate,
// but we remove them from the polling threads map here.
i.value()->requestInterruption();
i.remove();
} }
} }
@@ -535,12 +502,8 @@ signals:
void computerStateChanged(NvComputer* computer); void computerStateChanged(NvComputer* computer);
private: private:
void run() QString fetchServerInfo(NvHTTP& http)
{ {
NvHTTP http(m_Address);
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
QString serverInfo; QString serverInfo;
try { try {
// There's a race condition between GameStream servers reporting presence over // There's a race condition between GameStream servers reporting presence over
@@ -548,12 +511,12 @@ private:
// around this issue, we will issue the request again after a few seconds if // around this issue, we will issue the request again after a few seconds if
// we see a ServiceUnavailableError error. // we see a ServiceUnavailableError error.
try { try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE); serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
} catch (const QtNetworkReplyException& e) { } catch (const QtNetworkReplyException& e) {
if (e.getError() == QNetworkReply::ServiceUnavailableError) { if (e.getError() == QNetworkReply::ServiceUnavailableError) {
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError"; qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
QThread::sleep(5); QThread::sleep(5);
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE); serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
qInfo() << "Retry successful"; qInfo() << "Retry successful";
} }
else { else {
@@ -561,26 +524,74 @@ private:
throw e; throw e;
} }
} }
return serverInfo;
} catch (...) { } catch (...) {
if (!m_Mdns) { if (!m_Mdns) {
emit computerAddCompleted(false); emit computerAddCompleted(false);
} }
return QString();
}
}
void run()
{
NvHTTP http(m_Address, QSslCertificate());
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
QString serverInfo = fetchServerInfo(http);
if (serverInfo.isEmpty()) {
return; return;
} }
NvComputer* newComputer = new NvComputer(m_Address, serverInfo); // Create initial newComputer using HTTP serverinfo with no pinned cert
NvComputer* newComputer = new NvComputer(m_Address, serverInfo, QSslCertificate());
// Check if we have a record of this host UUID to pull the pinned cert
NvComputer* existingComputer;
{
QReadLocker lock(&m_ComputerManager->m_Lock);
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
if (existingComputer != nullptr) {
http.setServerCert(existingComputer->serverCert);
}
}
// Fetch serverinfo again over HTTPS with the pinned cert
if (existingComputer != nullptr) {
serverInfo = fetchServerInfo(http);
if (serverInfo.isEmpty()) {
return;
}
// Update the polled computer with the HTTPS information
NvComputer httpsComputer(m_Address, serverInfo, QSslCertificate());
newComputer->update(httpsComputer);
}
// Update addresses depending on the context // Update addresses depending on the context
if (m_Mdns) { if (m_Mdns) {
newComputer->localAddress = m_Address; newComputer->localAddress = m_Address;
// Get the WAN IP address using STUN if we're on mDNS
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
} }
else { else {
newComputer->manualAddress = m_Address; newComputer->manualAddress = m_Address;
} }
{
// Check if this PC already exists // Check if this PC already exists
QWriteLocker lock(&m_ComputerManager->m_Lock); QWriteLocker lock(&m_ComputerManager->m_Lock);
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid]; NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
if (existingComputer != nullptr) { if (existingComputer != nullptr) {
// Fold it into the existing PC // Fold it into the existing PC
bool changed = existingComputer->update(*newComputer); bool changed = existingComputer->update(*newComputer);
@@ -619,6 +630,7 @@ private:
emit computerStateChanged(newComputer); emit computerStateChanged(newComputer);
} }
} }
}
ComputerManager* m_ComputerManager; ComputerManager* m_ComputerManager;
QString m_Address; QString m_Address;
+74 -1
View File
@@ -51,6 +51,79 @@ private:
QMdnsEngine::Resolver m_Resolver; QMdnsEngine::Resolver m_Resolver;
}; };
class ComputerPollingEntry
{
public:
ComputerPollingEntry()
: m_ActiveThread(nullptr)
{
}
virtual ~ComputerPollingEntry()
{
interrupt();
// interrupt() should have taken care of this
Q_ASSERT(m_ActiveThread == nullptr);
for (QThread* thread : m_InactiveList) {
thread->wait();
delete thread;
}
}
bool isActive()
{
cleanInactiveList();
return m_ActiveThread != nullptr;
}
void setActiveThread(QThread* thread)
{
cleanInactiveList();
Q_ASSERT(!isActive());
m_ActiveThread = thread;
}
void interrupt()
{
cleanInactiveList();
if (m_ActiveThread != nullptr) {
// Interrupt the active thread
m_ActiveThread->requestInterruption();
// Place it on the inactive list awaiting death
m_InactiveList.append(m_ActiveThread);
m_ActiveThread = nullptr;
}
}
private:
void cleanInactiveList()
{
QMutableListIterator<QThread*> i(m_InactiveList);
// Reap any threads that have finished
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
if (thread->isFinished()) {
delete thread;
i.remove();
}
}
}
QThread* m_ActiveThread;
QList<QThread*> m_InactiveList;
};
class ComputerManager : public QObject class ComputerManager : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -100,7 +173,7 @@ private:
int m_PollingRef; int m_PollingRef;
QReadWriteLock m_Lock; QReadWriteLock m_Lock;
QMap<QString, NvComputer*> m_KnownHosts; QMap<QString, NvComputer*> m_KnownHosts;
QMap<QString, QThread*> m_PollThreads; QMap<QString, ComputerPollingEntry*> m_PollEntries;
QMdnsEngine::Server m_MdnsServer; QMdnsEngine::Server m_MdnsServer;
QMdnsEngine::Browser* m_MdnsBrowser; QMdnsEngine::Browser* m_MdnsBrowser;
QMdnsEngine::Cache m_MdnsCache; QMdnsEngine::Cache m_MdnsCache;
+59
View File
@@ -0,0 +1,59 @@
#include "computerseeker.h"
#include "computermanager.h"
#include <QTimer>
ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent)
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
m_TimeoutTimer(new QTimer(this))
{
m_TimeoutTimer->setSingleShot(true);
connect(m_TimeoutTimer, &QTimer::timeout,
this, &ComputerSeeker::onTimeout);
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
this, &ComputerSeeker::onComputerUpdated);
}
void ComputerSeeker::start(int timeout)
{
m_TimeoutTimer->start(timeout);
// Seek desired computer by both connecting to it directly (this may fail
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
// address) and by polling it using mDNS, hopefully one of these methods
// would find the host
m_ComputerManager->addNewHost(m_ComputerName, false);
m_ComputerManager->startPolling();
}
void ComputerSeeker::onComputerUpdated(NvComputer *computer)
{
if (!m_TimeoutTimer->isActive()) {
return;
}
if (matchComputer(computer) && isOnline(computer)) {
m_ComputerManager->stopPollingAsync();
m_TimeoutTimer->stop();
emit computerFound(computer);
}
}
bool ComputerSeeker::matchComputer(NvComputer *computer) const
{
QString value = m_ComputerName.toLower();
return computer->name.toLower() == value ||
computer->localAddress.toLower() == value ||
computer->remoteAddress.toLower() == value ||
computer->manualAddress.toLower() == value ||
computer->uuid.toLower() == value;
}
bool ComputerSeeker::isOnline(NvComputer *computer) const
{
return computer->state == NvComputer::CS_ONLINE;
}
void ComputerSeeker::onTimeout()
{
m_TimeoutTimer->stop();
m_ComputerManager->stopPollingAsync();
emit errorTimeout();
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <QObject>
class ComputerManager;
class NvComputer;
class QTimer;
class ComputerSeeker : public QObject
{
Q_OBJECT
public:
explicit ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent = nullptr);
void start(int timeout);
signals:
void computerFound(NvComputer *computer);
void errorTimeout();
private slots:
void onComputerUpdated(NvComputer *computer);
void onTimeout();
private:
bool matchComputer(NvComputer *computer) const;
bool isOnline(NvComputer *computer) const;
private:
ComputerManager *m_ComputerManager;
QString m_ComputerName;
QTimer *m_TimeoutTimer;
};
+44 -6
View File
@@ -2,6 +2,7 @@
#include <QUdpSocket> #include <QUdpSocket>
#include <QHostInfo> #include <QHostInfo>
#include <QNetworkInterface>
#define SER_NAME "hostname" #define SER_NAME "hostname"
#define SER_UUID "uuid" #define SER_UUID "uuid"
@@ -10,6 +11,7 @@
#define SER_REMOTEADDR "remoteaddress" #define SER_REMOTEADDR "remoteaddress"
#define SER_MANUALADDR "manualaddress" #define SER_MANUALADDR "manualaddress"
#define SER_APPLIST "apps" #define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert"
#define SER_APPNAME "name" #define SER_APPNAME "name"
#define SER_APPID "id" #define SER_APPID "id"
@@ -23,6 +25,7 @@ NvComputer::NvComputer(QSettings& settings)
this->localAddress = settings.value(SER_LOCALADDR).toString(); this->localAddress = settings.value(SER_LOCALADDR).toString();
this->remoteAddress = settings.value(SER_REMOTEADDR).toString(); this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
this->manualAddress = settings.value(SER_MANUALADDR).toString(); this->manualAddress = settings.value(SER_MANUALADDR).toString();
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
int appCount = settings.beginReadArray(SER_APPLIST); int appCount = settings.beginReadArray(SER_APPLIST);
for (int i = 0; i < appCount; i++) { for (int i = 0; i < appCount; i++) {
@@ -61,6 +64,7 @@ void NvComputer::serialize(QSettings& settings)
settings.setValue(SER_LOCALADDR, localAddress); settings.setValue(SER_LOCALADDR, localAddress);
settings.setValue(SER_REMOTEADDR, remoteAddress); settings.setValue(SER_REMOTEADDR, remoteAddress);
settings.setValue(SER_MANUALADDR, manualAddress); settings.setValue(SER_MANUALADDR, manualAddress);
settings.setValue(SER_SRVCERT, serverCert.toPem());
// Avoid deleting an existing applist if we couldn't get one // Avoid deleting an existing applist if we couldn't get one
if (!appList.isEmpty()) { if (!appList.isEmpty()) {
@@ -84,8 +88,10 @@ void NvComputer::sortAppList()
}); });
} }
NvComputer::NvComputer(QString address, QString serverInfo) NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serverCert)
{ {
this->serverCert = serverCert;
this->name = NvHTTP::getXmlString(serverInfo, "hostname"); this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isEmpty()) { if (this->name.isEmpty()) {
this->name = "UNKNOWN"; this->name = "UNKNOWN";
@@ -167,6 +173,32 @@ bool NvComputer::wake()
QVector<QString> addressList = uniqueAddresses(); QVector<QString> addressList = uniqueAddresses();
addressList.append("255.255.255.255"); addressList.append("255.255.255.255");
// Try to broadcast on all available NICs
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
// Ensure the interface is up and skip the loopback adapter
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
continue;
}
QHostAddress allNodesMulticast("FF02::1");
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
// Store the scope ID for this NIC if IPv6 is enabled
if (!addr.ip().scopeId().isEmpty()) {
allNodesMulticast.setScopeId(addr.ip().scopeId());
}
// Skip IPv6 which doesn't support broadcast
if (!addr.broadcast().isNull()) {
addressList.append(addr.broadcast().toString());
}
}
if (!allNodesMulticast.scopeId().isEmpty()) {
addressList.append(allNodesMulticast.toString());
}
}
// Try all unique address strings or host names // Try all unique address strings or host names
bool success = false; bool success = false;
for (QString& addressString : addressList) { for (QString& addressString : addressList) {
@@ -181,16 +213,14 @@ bool NvComputer::wake()
for (QHostAddress& address : hostInfo.addresses()) { for (QHostAddress& address : hostInfo.addresses()) {
QUdpSocket sock; QUdpSocket sock;
// Bind to any address on the correct protocol
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
// 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)) {
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port; qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
success = true; success = true;
} }
else {
qWarning() << "Send failed:" << sock.error();
} }
} }
} }
@@ -255,6 +285,13 @@ bool NvComputer::update(NvComputer& that)
changed = true; \ changed = true; \
} }
#define ASSIGN_IF_CHANGED_AND_NONNULL(field) \
if (!that.field.isNull() && \
this->field != that.field) { \
this->field = that.field; \
changed = true; \
}
ASSIGN_IF_CHANGED(name); ASSIGN_IF_CHANGED(name);
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress); ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress); ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
@@ -269,6 +306,7 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(appVersion); ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC); ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel); ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
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;
+2 -1
View File
@@ -19,7 +19,7 @@ private:
bool pendingQuit; bool pendingQuit;
public: public:
explicit NvComputer(QString address, QString serverInfo); explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
explicit NvComputer(QSettings& settings); explicit NvComputer(QSettings& settings);
@@ -68,6 +68,7 @@ public:
QByteArray macAddress; QByteArray macAddress;
QString name; QString name;
QString uuid; QString uuid;
QSslCertificate serverCert;
QVector<NvApp> appList; QVector<NvApp> appList;
// Synchronization // Synchronization
+89 -26
View File
@@ -10,11 +10,16 @@
#include <QSslKey> #include <QSslKey>
#include <QImageReader> #include <QImageReader>
#include <QtEndian> #include <QtEndian>
#include <QNetworkProxy>
#define REQUEST_TIMEOUT_MS 5000 #define REQUEST_TIMEOUT_MS 5000
#define LAUNCH_TIMEOUT_MS 120000
#define RESUME_TIMEOUT_MS 30000
#define QUIT_TIMEOUT_MS 30000
NvHTTP::NvHTTP(QString address) : NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
m_Address(address) m_Address(address),
m_ServerCert(serverCert)
{ {
Q_ASSERT(!address.isEmpty()); Q_ASSERT(!address.isEmpty());
@@ -24,6 +29,15 @@ NvHTTP::NvHTTP(QString address) :
m_BaseUrlHttps.setHost(address); m_BaseUrlHttps.setHost(address);
m_BaseUrlHttp.setPort(47989); m_BaseUrlHttp.setPort(47989);
m_BaseUrlHttps.setPort(47984); m_BaseUrlHttps.setPort(47984);
// Never use a proxy server
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
m_Nam.setProxy(noProxy);
}
void NvHTTP::setServerCert(QSslCertificate serverCert)
{
m_ServerCert = serverCert;
} }
QVector<int> QVector<int>
@@ -68,6 +82,9 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
{ {
QString serverInfo; QString serverInfo;
// Check if we have a pinned cert for this host yet
if (!m_ServerCert.isNull())
{
try try
{ {
// Always try HTTPS first, since it properly reports // Always try HTTPS first, since it properly reports
@@ -75,7 +92,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
serverInfo = openConnectionToString(m_BaseUrlHttps, serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo", "serverinfo",
nullptr, nullptr,
true, REQUEST_TIMEOUT_MS,
logLevel); logLevel);
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(serverInfo); verifyResponseStatus(serverInfo);
@@ -88,7 +105,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
serverInfo = openConnectionToString(m_BaseUrlHttp, serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo", "serverinfo",
nullptr, nullptr,
true, REQUEST_TIMEOUT_MS,
logLevel); logLevel);
verifyResponseStatus(serverInfo); verifyResponseStatus(serverInfo);
} }
@@ -98,6 +115,17 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
throw e; throw e;
} }
} }
}
else
{
// Only use HTTP prior to pairing
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
REQUEST_TIMEOUT_MS,
logLevel);
verifyResponseStatus(serverInfo);
}
return serverInfo; return serverInfo;
} }
@@ -144,7 +172,10 @@ NvHTTP::launchApp(int appId,
"appid="+QString::number(appId)+ "appid="+QString::number(appId)+
"&mode="+QString::number(streamConfig->width)+"x"+ "&mode="+QString::number(streamConfig->width)+"x"+
QString::number(streamConfig->height)+"x"+ QString::number(streamConfig->height)+"x"+
QString::number(streamConfig->fps)+ // Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 60 when starting. This won't impact our ability
// to get > 60 FPS while actually streaming though.
QString::number(streamConfig->fps > 60 ? 60 : 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(riKeyId)+ "&rikeyid="+QString::number(riKeyId)+
@@ -155,7 +186,7 @@ NvHTTP::launchApp(int appId,
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+ "&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
"&remoteControllersBitmap="+QString::number(gamepadMask)+ "&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask), "&gcmap="+QString::number(gamepadMask),
false); LAUNCH_TIMEOUT_MS);
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(response); verifyResponseStatus(response);
@@ -175,7 +206,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+ "rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(riKeyId)+ "&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration), "&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
false); RESUME_TIMEOUT_MS);
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(response); verifyResponseStatus(response);
@@ -188,14 +219,14 @@ NvHTTP::quitApp()
openConnectionToString(m_BaseUrlHttps, openConnectionToString(m_BaseUrlHttps,
"cancel", "cancel",
nullptr, nullptr,
false); QUIT_TIMEOUT_MS);
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(response); verifyResponseStatus(response);
// 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(NvHTTP::ERROR)) != 0) { if (getCurrentGame(getServerInfo(NvHTTP::NVLL_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, "");
@@ -235,8 +266,8 @@ NvHTTP::getAppList()
QString appxml = openConnectionToString(m_BaseUrlHttps, QString appxml = openConnectionToString(m_BaseUrlHttps,
"applist", "applist",
nullptr, nullptr,
true, REQUEST_TIMEOUT_MS,
NvLogLevel::ERROR); NvLogLevel::NVLL_ERROR);
verifyResponseStatus(appxml); verifyResponseStatus(appxml);
QXmlStreamReader xmlReader(appxml); QXmlStreamReader xmlReader(appxml);
@@ -303,8 +334,8 @@ NvHTTP::getBoxArt(int appId)
"appasset", "appasset",
"appid="+QString::number(appId)+ "appid="+QString::number(appId)+
"&AssetType=2&AssetIdx=0", "&AssetType=2&AssetIdx=0",
true, REQUEST_TIMEOUT_MS,
NvLogLevel::VERBOSE); NvLogLevel::NVLL_VERBOSE);
QImage image = QImageReader(reply).read(); QImage image = QImageReader(reply).read();
delete reply; delete reply;
@@ -350,10 +381,10 @@ QString
NvHTTP::openConnectionToString(QUrl baseUrl, NvHTTP::openConnectionToString(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout, int timeoutMs,
NvLogLevel logLevel) NvLogLevel logLevel)
{ {
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel); QNetworkReply* reply = openConnection(baseUrl, command, arguments, timeoutMs, logLevel);
QString ret; QString ret;
QTextStream stream(reply); QTextStream stream(reply);
@@ -368,14 +399,18 @@ QNetworkReply*
NvHTTP::openConnection(QUrl baseUrl, NvHTTP::openConnection(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout, int timeoutMs,
NvLogLevel logLevel) NvLogLevel logLevel)
{ {
// Build a URL for the request // Build a URL for the request
QUrl url(baseUrl); QUrl url(baseUrl);
url.setPath("/" + command); url.setPath("/" + command);
url.setQuery("uniqueid=" + IdentityManager::get()->getUniqueId() +
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() + // Use a common UID for Moonlight clients to allow them to quit
// games for each other (otherwise GFE gets screwed up and it requires
// manual intervention to solve).
url.setQuery("uniqueid=0123456789ABCDEF&uuid=" +
QUuid::createUuid().toRfc4122().toHex() +
((arguments != nullptr) ? ("&" + arguments) : "")); ((arguments != nullptr) ? ("&" + arguments) : ""));
QNetworkRequest request = QNetworkRequest(url); QNetworkRequest request = QNetworkRequest(url);
@@ -385,17 +420,30 @@ NvHTTP::openConnection(QUrl baseUrl,
QNetworkReply* reply = m_Nam.get(request); QNetworkReply* reply = m_Nam.get(request);
// Ignore self-signed certificate errors (since GFE uses them) // Assert that we always have a server cert for HTTPS, since
reply->ignoreSslErrors(); // the request will fail anyway if we do not.
Q_ASSERT(!m_ServerCert.isNull() || baseUrl == m_BaseUrlHttp);
if (!m_ServerCert.isNull()) {
// Pin the server certificate received during pairing
QList<QSslError> expectedSslErrors;
expectedSslErrors.append(QSslError(QSslError::HostNameMismatch, m_ServerCert));
expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, m_ServerCert));
// The SecureTransport backend for Qt TLS on macOS throws CertificateUntrusted
// instead of SelfSignedCertificate, so we will need to allow that error too.
expectedSslErrors.append(QSslError(QSslError::CertificateUntrusted, m_ServerCert));
reply->ignoreSslErrors(expectedSslErrors);
}
// Run the request with a timeout if requested // Run the request with a timeout if requested
QEventLoop loop; QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
if (enableTimeout) if (timeoutMs) {
{ QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
} }
if (logLevel >= NvLogLevel::VERBOSE) { if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
qInfo() << "Executing request:" << url.toString(); qInfo() << "Executing request:" << url.toString();
} }
loop.exec(QEventLoop::ExcludeUserInputEvents); loop.exec(QEventLoop::ExcludeUserInputEvents);
@@ -403,7 +451,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Abort the request if it timed out // Abort the request if it timed out
if (!reply->isFinished()) if (!reply->isFinished())
{ {
if (logLevel >= NvLogLevel::ERROR) { if (logLevel >= NvLogLevel::NVLL_ERROR) {
qWarning() << "Aborting timed out request for" << url.toString(); qWarning() << "Aborting timed out request for" << url.toString();
} }
reply->abort(); reply->abort();
@@ -416,13 +464,28 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error // Handle error
if (reply->error() != QNetworkReply::NoError) if (reply->error() != QNetworkReply::NoError)
{ {
if (logLevel >= NvLogLevel::ERROR) { if (logLevel >= NvLogLevel::NVLL_ERROR) {
qWarning() << command << " request failed with error " << reply->error(); qWarning() << command << " request failed with error " << reply->error();
} }
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
// This will trigger falling back to HTTP for the serverinfo query
// then pairing again to get the updated certificate.
GfeHttpResponseException exception(401, "Server certificate mismatch");
delete reply;
throw exception;
}
else if (reply->error() == QNetworkReply::OperationCanceledError) {
QtNetworkReplyException exception(QNetworkReply::TimeoutError, "Request timed out");
delete reply;
throw exception;
}
else {
QtNetworkReplyException exception(reply->error(), reply->errorString()); QtNetworkReplyException exception(reply->error(), reply->errorString());
delete reply; delete reply;
throw exception; throw exception;
} }
}
return reply; return reply;
} }
+10 -7
View File
@@ -117,12 +117,12 @@ class NvHTTP
{ {
public: public:
enum NvLogLevel { enum NvLogLevel {
NONE, NVLL_NONE,
ERROR, NVLL_ERROR,
VERBOSE NVLL_VERBOSE
}; };
explicit NvHTTP(QString address); explicit NvHTTP(QString address, QSslCertificate serverCert);
static static
int int
@@ -149,8 +149,10 @@ public:
openConnectionToString(QUrl baseUrl, openConnectionToString(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout, int timeoutMs,
NvLogLevel logLevel = NvLogLevel::VERBOSE); NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE);
void setServerCert(QSslCertificate serverCert);
static static
QVector<int> QVector<int>
@@ -186,9 +188,10 @@ private:
openConnection(QUrl baseUrl, openConnection(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout, int timeoutMs,
NvLogLevel logLevel); NvLogLevel logLevel);
QString m_Address; QString m_Address;
QNetworkAccessManager m_Nam; QNetworkAccessManager m_Nam;
QSslCertificate m_ServerCert;
}; };
+32 -18
View File
@@ -8,8 +8,10 @@
#include <openssl/x509.h> #include <openssl/x509.h>
#include <openssl/evp.h> #include <openssl/evp.h>
#define REQUEST_TIMEOUT_MS 5000
NvPairingManager::NvPairingManager(QString address) : NvPairingManager::NvPairingManager(QString address) :
m_Http(address) m_Http(address, QSslCertificate())
{ {
QByteArray cert = IdentityManager::get()->getCertificate(); QByteArray cert = IdentityManager::get()->getCertificate();
BIO *bio = BIO_new_mem_buf(cert.data(), -1); BIO *bio = BIO_new_mem_buf(cert.data(), -1);
@@ -161,7 +163,7 @@ NvPairingManager::saltPin(QByteArray salt, QString pin)
} }
NvPairingManager::PairState NvPairingManager::PairState
NvPairingManager::pair(QString appVersion, QString pin) NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert)
{ {
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0); int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
qInfo() << "Pairing with server generation:" << serverMajorVersion; qInfo() << "Pairing with server generation:" << serverMajorVersion;
@@ -192,7 +194,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
"pair", "pair",
"devicename=roth&updateState=1&phrase=getservercert&salt=" + "devicename=roth&updateState=1&phrase=getservercert&salt=" +
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(), salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
false); 0);
NvHTTP::verifyResponseStatus(getCert); NvHTTP::verifyResponseStatus(getCert);
if (NvHTTP::getXmlString(getCert, "paired") != "1") if (NvHTTP::getXmlString(getCert, "paired") != "1")
{ {
@@ -200,26 +202,38 @@ NvPairingManager::pair(QString appVersion, QString pin)
return PairState::FAILED; return PairState::FAILED;
} }
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert"); QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
if (serverCert == nullptr) if (serverCertStr == nullptr)
{ {
qCritical() << "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, REQUEST_TIMEOUT_MS);
return PairState::ALREADY_IN_PROGRESS; return PairState::ALREADY_IN_PROGRESS;
} }
serverCert = QSslCertificate(serverCertStr);
if (serverCert.isNull()) {
Q_ASSERT(!serverCert.isNull());
qCritical() << "Failed to parse plaincert";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED;
}
// Pin this cert for TLS
m_Http.setServerCert(serverCert);
QByteArray randomChallenge = generateRandomBytes(16); QByteArray randomChallenge = generateRandomBytes(16);
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey); QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair", "pair",
"devicename=roth&updateState=1&clientchallenge=" + "devicename=roth&updateState=1&clientchallenge=" +
encryptedChallenge.toHex(), encryptedChallenge.toHex(),
true); REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(challengeXml); NvHTTP::verifyResponseStatus(challengeXml);
if (NvHTTP::getXmlString(challengeXml, "paired") != "1") if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
{ {
qCritical() << "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, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
@@ -247,12 +261,12 @@ NvPairingManager::pair(QString appVersion, QString pin)
"pair", "pair",
"devicename=roth&updateState=1&serverchallengeresp=" + "devicename=roth&updateState=1&serverchallengeresp=" +
encryptedChallengeResponseHash.toHex(), encryptedChallengeResponseHash.toHex(),
true); REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(respXml); NvHTTP::verifyResponseStatus(respXml);
if (NvHTTP::getXmlString(respXml, "paired") != "1") if (NvHTTP::getXmlString(respXml, "paired") != "1")
{ {
qCritical() << "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, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
@@ -262,21 +276,21 @@ NvPairingManager::pair(QString appVersion, QString pin)
if (!verifySignature(serverSecret, if (!verifySignature(serverSecret,
serverSignature, serverSignature,
serverCert)) serverCertStr))
{ {
qCritical() << "MITM detected"; qCritical() << "MITM detected";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
QByteArray expectedResponseData; QByteArray expectedResponseData;
expectedResponseData.append(randomChallenge); expectedResponseData.append(randomChallenge);
expectedResponseData.append(getSignatureFromPemCert(serverCert)); expectedResponseData.append(getSignatureFromPemCert(serverCertStr));
expectedResponseData.append(serverSecret); expectedResponseData.append(serverSecret);
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse) if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
{ {
qCritical() << "Incorrect PIN"; qCritical() << "Incorrect PIN";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::PIN_WRONG; return PairState::PIN_WRONG;
} }
@@ -288,24 +302,24 @@ NvPairingManager::pair(QString appVersion, QString pin)
"pair", "pair",
"devicename=roth&updateState=1&clientpairingsecret=" + "devicename=roth&updateState=1&clientpairingsecret=" +
clientPairingSecret.toHex(), clientPairingSecret.toHex(),
true); REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(secretRespXml); NvHTTP::verifyResponseStatus(secretRespXml);
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1") if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
{ {
qCritical() << "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, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps, QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
"pair", "pair",
"devicename=roth&updateState=1&phrase=pairchallenge", "devicename=roth&updateState=1&phrase=pairchallenge",
true); REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(pairChallengeXml); NvHTTP::verifyResponseStatus(pairChallengeXml);
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1") if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
{ {
qCritical() << "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, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
+1 -1
View File
@@ -23,7 +23,7 @@ public:
~NvPairingManager(); ~NvPairingManager();
PairState PairState
pair(QString appVersion, QString pin); pair(QString appVersion, QString pin, QSslCertificate& serverCert);
private: private:
QByteArray QByteArray
+53 -1
View File
@@ -164,6 +164,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
"Starts Moonlight normally if no arguments are given.\n" "Starts Moonlight normally if no arguments are given.\n"
"\n" "\n"
"Available actions:\n" "Available actions:\n"
" quit Quit the currently running app\n"
" stream Start streaming an app\n" " stream Start streaming an app\n"
"\n" "\n"
"See 'moonlight <action> --help' for help of specific action." "See 'moonlight <action> --help' for help of specific action."
@@ -179,6 +180,8 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
parser.handleHelpAndVersionOptions(); parser.handleHelpAndVersionOptions();
parser.handleUnknownOptions(); parser.handleUnknownOptions();
return NormalStartRequested; return NormalStartRequested;
} else if (action == "quit") {
return QuitRequested;
} else if (action == "stream") { } else if (action == "stream") {
return StreamRequested; return StreamRequested;
} else { } else {
@@ -186,6 +189,48 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
} }
} }
QuitCommandLineParser::QuitCommandLineParser()
{
}
QuitCommandLineParser::~QuitCommandLineParser()
{
}
void QuitCommandLineParser::parse(const QStringList &args)
{
CommandLineParser parser;
parser.setupCommonOptions();
parser.setApplicationDescription(
"\n"
"Quit the currently running app on the given host."
);
parser.addPositionalArgument("quit", "quit running app");
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
if (!parser.parse(args)) {
parser.showError(parser.errorText());
}
parser.handleUnknownOptions();
// This method will not return and terminates the process if --version or
// --help is specified
parser.handleHelpAndVersionOptions();
// Verify that host has been provided
auto posArgs = parser.positionalArguments();
if (posArgs.length() < 2) {
parser.showError("Host not provided");
}
m_Host = parser.positionalArguments().at(1);
}
QString QuitCommandLineParser::getHost() const
{
return m_Host;
}
StreamCommandLineParser::StreamCommandLineParser() StreamCommandLineParser::StreamCommandLineParser()
{ {
m_WindowModeMap = { m_WindowModeMap = {
@@ -238,9 +283,11 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys()); parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys()); parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
parser.addToggleOption("multi-controller", "multiple controller support"); parser.addToggleOption("multi-controller", "multiple controller support");
parser.addToggleOption("quit-after", "quit app after session");
parser.addToggleOption("mouse-acceleration", "mouse acceleration"); parser.addToggleOption("mouse-acceleration", "mouse acceleration");
parser.addToggleOption("game-optimization", "game optimizations"); parser.addToggleOption("game-optimization", "game optimizations");
parser.addToggleOption("audio-on-host", "audio on host PC"); parser.addToggleOption("audio-on-host", "audio on host PC");
parser.addToggleOption("frame-pacing", "frame pacing");
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys()); parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys()); parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
@@ -314,6 +361,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --multi-controller and --no-multi-controller options // Resolve --multi-controller and --no-multi-controller options
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController); preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
// Resolve --quit-after and --no-quit-after options
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
// Resolve --mouse-acceleration and --no-mouse-acceleration options // Resolve --mouse-acceleration and --no-mouse-acceleration options
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration); preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
@@ -323,6 +373,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --audio-on-host and --no-audio-on-host options // Resolve --audio-on-host and --no-audio-on-host options
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost); preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
// Resolve --frame-pacing and --no-frame-pacing options
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
// Resolve --video-codec option // Resolve --video-codec option
if (parser.isSet("video-codec")) { if (parser.isSet("video-codec")) {
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec")); preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
@@ -359,4 +412,3 @@ QString StreamCommandLineParser::getAppName() const
{ {
return m_AppName; return m_AppName;
} }
+15
View File
@@ -11,6 +11,7 @@ public:
enum ParseResult { enum ParseResult {
NormalStartRequested, NormalStartRequested,
StreamRequested, StreamRequested,
QuitRequested,
}; };
GlobalCommandLineParser(); GlobalCommandLineParser();
@@ -20,6 +21,20 @@ public:
}; };
class QuitCommandLineParser
{
public:
QuitCommandLineParser();
virtual ~QuitCommandLineParser();
void parse(const QStringList &args);
QString getHost() const;
private:
QString m_Host;
};
class StreamCommandLineParser class StreamCommandLineParser
{ {
public: public:
+171
View File
@@ -0,0 +1,171 @@
#include "quitstream.h"
#include "backend/computermanager.h"
#include "backend/computerseeker.h"
#include "streaming/session.h"
#include <QCoreApplication>
#include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 10000
namespace CliQuitStream
{
enum State {
StateInit,
StateSeekComputer,
StateQuitApp,
StateFailure,
};
class Event
{
public:
enum Type {
AppQuitCompleted,
ComputerFound,
ComputerSeekTimedout,
Executed,
};
Event(Type type)
: type(type), computerManager(nullptr), computer(nullptr) {}
Type type;
ComputerManager *computerManager;
NvComputer *computer;
QString errorMessage;
};
class LauncherPrivate
{
Q_DECLARE_PUBLIC(Launcher)
public:
LauncherPrivate(Launcher *q) : q_ptr(q) {}
void handleEvent(Event event)
{
Q_Q(Launcher);
NvApp app;
switch (event.type) {
// Occurs when CliQuitStreamSegue becomes visible and the UI calls launcher's execute()
case Event::Executed:
if (m_State == StateInit) {
m_State = StateSeekComputer;
m_ComputerManager = event.computerManager;
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
q, &Launcher::onQuitAppCompleted);
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
q, &Launcher::onComputerFound);
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
q, &Launcher::onComputerSeekTimeout);
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
emit q->searchingComputer();
}
break;
// Occurs when computer search timed out
case Event::ComputerSeekTimedout:
if (m_State == StateSeekComputer) {
m_State = StateFailure;
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
}
break;
// Occurs when searched computer is found
case Event::ComputerFound:
if (m_State == StateSeekComputer) {
if (event.computer->pairState == NvComputer::PS_PAIRED) {
m_State = StateQuitApp;
emit q->quittingApp();
m_ComputerManager->quitRunningApp(event.computer);
} else {
m_State = StateFailure;
QString msg = QString("Computer %1 has not been paired. "
"Please open Moonlight to pair before streaming.")
.arg(event.computer->name);
emit q->failed(msg);
}
}
break;
// Occurs when app quit completed (error message is set if failed)
case Event::AppQuitCompleted:
if (m_State == StateQuitApp) {
if (event.errorMessage.isEmpty()) {
QCoreApplication::exit(0);
} else {
m_State = StateFailure;
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
}
}
break;
}
}
Launcher *q_ptr;
ComputerManager *m_ComputerManager;
QString m_ComputerName;
ComputerSeeker *m_ComputerSeeker;
State m_State;
QTimer *m_TimeoutTimer;
};
Launcher::Launcher(QString computer, QObject *parent)
: QObject(parent),
m_DPtr(new LauncherPrivate(this))
{
Q_D(Launcher);
d->m_ComputerName = computer;
d->m_State = StateInit;
d->m_TimeoutTimer = new QTimer(this);
d->m_TimeoutTimer->setSingleShot(true);
connect(d->m_TimeoutTimer, &QTimer::timeout,
this, &Launcher::onComputerSeekTimeout);
}
Launcher::~Launcher()
{
}
void Launcher::execute(ComputerManager *manager)
{
Q_D(Launcher);
Event event(Event::Executed);
event.computerManager = manager;
d->handleEvent(event);
}
bool Launcher::isExecuted() const
{
Q_D(const Launcher);
return d->m_State != StateInit;
}
void Launcher::onComputerFound(NvComputer *computer)
{
Q_D(Launcher);
Event event(Event::ComputerFound);
event.computer = computer;
d->handleEvent(event);
}
void Launcher::onComputerSeekTimeout()
{
Q_D(Launcher);
Event event(Event::ComputerSeekTimedout);
d->handleEvent(event);
}
void Launcher::onQuitAppCompleted(QVariant error)
{
Q_D(Launcher);
Event event(Event::AppQuitCompleted);
event.errorMessage = error.toString();
d->handleEvent(event);
}
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <QObject>
#include <QVariant>
class ComputerManager;
class NvComputer;
namespace CliQuitStream
{
class LauncherPrivate;
class Launcher : public QObject
{
Q_OBJECT
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
public:
explicit Launcher(QString computer, QObject *parent = nullptr);
~Launcher();
Q_INVOKABLE void execute(ComputerManager *manager);
Q_INVOKABLE bool isExecuted() const;
signals:
void searchingComputer();
void quittingApp();
void failed(QString text);
private slots:
void onComputerFound(NvComputer *computer);
void onComputerSeekTimeout();
void onQuitAppCompleted(QVariant error);
private:
QScopedPointer<LauncherPrivate> m_DPtr;
};
}
+74 -38
View File
@@ -1,7 +1,9 @@
#include "startstream.h" #include "startstream.h"
#include "backend/computermanager.h" #include "backend/computermanager.h"
#include "backend/computerseeker.h"
#include "streaming/session.h" #include "streaming/session.h"
#include <QCoreApplication>
#include <QTimer> #include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 10000 #define COMPUTER_SEEK_TIMEOUT 10000
@@ -22,6 +24,9 @@ class Event
{ {
public: public:
enum Type { enum Type {
AppQuitCompleted,
AppQuitRequested,
ComputerFound,
ComputerUpdated, ComputerUpdated,
Executed, Executed,
Timedout, Timedout,
@@ -33,6 +38,7 @@ public:
Type type; Type type;
ComputerManager *computerManager; ComputerManager *computerManager;
NvComputer *computer; NvComputer *computer;
QString errorMessage;
}; };
class LauncherPrivate class LauncherPrivate
@@ -49,30 +55,34 @@ public:
NvApp app; NvApp app;
switch (event.type) { switch (event.type) {
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute()
case Event::Executed: case Event::Executed:
if (m_State == StateInit) { if (m_State == StateInit) {
m_State = StateSeekComputer; m_State = StateSeekComputer;
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
m_ComputerManager = event.computerManager; m_ComputerManager = event.computerManager;
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
q, &Launcher::onComputerFound);
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
q, &Launcher::onTimeout);
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged, q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
q, &Launcher::onComputerUpdated); q, &Launcher::onComputerUpdated);
// Seek desired computer by both connecting to it directly (this may fail q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP q, &Launcher::onQuitAppCompleted);
// address) and by polling it using mDNS, hopefully one of these methods
// would find the host
m_ComputerManager->addNewHost(m_ComputerName, false);
m_ComputerManager->startPolling();
emit q->searchingComputer(); emit q->searchingComputer();
} }
break; break;
case Event::ComputerUpdated: // Occurs when searched computer is found
case Event::ComputerFound:
if (m_State == StateSeekComputer) { if (m_State == StateSeekComputer) {
if (matchComputer(event.computer) && isOnline(event.computer)) { if (event.computer->pairState == NvComputer::PS_PAIRED) {
if (isPaired(event.computer)) {
m_State = StateSeekApp; m_State = StateSeekApp;
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
m_Computer = event.computer; m_Computer = event.computer;
m_ComputerManager->stopPollingAsync(); m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
emit q->searchingApp(); emit q->searchingApp();
} else { } else {
m_State = StateFailure; m_State = StateFailure;
@@ -82,25 +92,41 @@ public:
emit q->failed(msg); emit q->failed(msg);
} }
} }
} break;
// Occurs when a computer is updated
case Event::ComputerUpdated:
if (m_State == StateSeekApp) { if (m_State == StateSeekApp) {
int index = getAppIndex(); int index = getAppIndex();
if (-1 != index) { if (-1 != index) {
app = m_Computer->appList[index]; app = m_Computer->appList[index];
m_TimeoutTimer->stop();
if (isNotStreaming() || isStreamingApp(app)) { if (isNotStreaming() || isStreamingApp(app)) {
m_State = StateStartSession; m_State = StateStartSession;
m_TimeoutTimer->stop();
session = new Session(m_Computer, app, m_Preferences); session = new Session(m_Computer, app, m_Preferences);
emit q->sessionCreated(app.name, session); emit q->sessionCreated(app.name, session);
} else { } else {
m_State = StateFailure; emit q->appQuitRequired(getCurrentAppName());
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
.arg(getCurrentAppName(), app.name);
emit q->failed(msg);
} }
} }
} }
break; break;
// Occurs when there was another app running on computer and user accepted quit
// confirmation dialog
case Event::AppQuitRequested:
if (m_State == StateSeekApp) {
m_ComputerManager->quitRunningApp(m_Computer);
}
break;
// Occurs when the previous app quit has been completed, handles quitting errors if any
// happended. ComputerUpdated event's handler handles session start when previous app has
// quit.
case Event::AppQuitCompleted:
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
m_State = StateFailure;
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
}
break;
// Occurs when computer or app search timed out
case Event::Timedout: case Event::Timedout:
if (m_State == StateSeekComputer) { if (m_State == StateSeekComputer) {
m_State = StateFailure; m_State = StateFailure;
@@ -114,26 +140,6 @@ public:
} }
} }
bool matchComputer(NvComputer *computer) const
{
QString value = m_ComputerName.toLower();
return computer->name.toLower() == value ||
computer->localAddress.toLower() == value ||
computer->remoteAddress.toLower() == value ||
computer->manualAddress.toLower() == value ||
computer->uuid.toLower() == value;
}
bool isOnline(NvComputer *computer) const
{
return computer->state == NvComputer::CS_ONLINE;
}
bool isPaired(NvComputer *computer) const
{
return computer->pairState == NvComputer::PS_PAIRED;
}
int getAppIndex() const int getAppIndex() const
{ {
for (int i = 0; i < m_Computer->appList.length(); i++) { for (int i = 0; i < m_Computer->appList.length(); i++) {
@@ -169,6 +175,7 @@ public:
QString m_AppName; QString m_AppName;
StreamingPreferences *m_Preferences; StreamingPreferences *m_Preferences;
ComputerManager *m_ComputerManager; ComputerManager *m_ComputerManager;
ComputerSeeker *m_ComputerSeeker;
NvComputer *m_Computer; NvComputer *m_Computer;
State m_State; State m_State;
QTimer *m_TimeoutTimer; QTimer *m_TimeoutTimer;
@@ -202,6 +209,27 @@ void Launcher::execute(ComputerManager *manager)
d->handleEvent(event); d->handleEvent(event);
} }
void Launcher::quitRunningApp()
{
Q_D(Launcher);
Event event(Event::AppQuitRequested);
d->handleEvent(event);
}
bool Launcher::isExecuted() const
{
Q_D(const Launcher);
return d->m_State != StateInit;
}
void Launcher::onComputerFound(NvComputer *computer)
{
Q_D(Launcher);
Event event(Event::ComputerFound);
event.computer = computer;
d->handleEvent(event);
}
void Launcher::onComputerUpdated(NvComputer *computer) void Launcher::onComputerUpdated(NvComputer *computer)
{ {
Q_D(Launcher); Q_D(Launcher);
@@ -217,4 +245,12 @@ void Launcher::onTimeout()
d->handleEvent(event); d->handleEvent(event);
} }
void Launcher::onQuitAppCompleted(QVariant error)
{
Q_D(Launcher);
Event event(Event::AppQuitCompleted);
event.errorMessage = error.toString();
d->handleEvent(event);
}
} }
+6
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QVariant>
class ComputerManager; class ComputerManager;
class NvComputer; class NvComputer;
@@ -24,16 +25,21 @@ public:
QObject *parent = nullptr); QObject *parent = nullptr);
~Launcher(); ~Launcher();
Q_INVOKABLE void execute(ComputerManager *manager); Q_INVOKABLE void execute(ComputerManager *manager);
Q_INVOKABLE void quitRunningApp();
Q_INVOKABLE bool isExecuted() const;
signals: signals:
void searchingComputer(); void searchingComputer();
void searchingApp(); void searchingApp();
void sessionCreated(QString appName, Session *session); void sessionCreated(QString appName, Session *session);
void failed(QString text); void failed(QString text);
void appQuitRequired(QString appName);
private slots: private slots:
void onComputerFound(NvComputer *computer);
void onComputerUpdated(NvComputer *computer); void onComputerUpdated(NvComputer *computer);
void onTimeout(); void onTimeout();
void onQuitAppCompleted(QVariant error);
private: private:
QScopedPointer<LauncherPrivate> m_DPtr; QScopedPointer<LauncherPrivate> m_DPtr;
@@ -4,7 +4,7 @@
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license> <project_license>GPL-3.0+</project_license>
<name>Moonlight</name> <name>Moonlight</name>
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary> <summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
<description> <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>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>
@@ -19,20 +19,139 @@
</ul> </ul>
</description> </description>
<url type="homepage">https://moonlight-stream.com</url> <url type="homepage">https://moonlight-stream.org</url>
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</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> <url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image> <image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image> <image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="0.9.1" date="2019-02-17">
<description>
<p>New features:</p>
<ul>
<li>Added gamepad rumble support</li>
<li>Added a dedicated quit button to the running app tile</li>
<li>Reduced video latency on Windows and Linux</li>
<li>Added a performance overlay for software decoding</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed delayed first video frame with GFE 3.17</li>
<li>Fixed erroneously setting host resolution to 720p in certain configurations</li>
<li>The connection termination dialog is no longer displayed for intentional termination</li>
<li>Fixed some gamepads only functioning for UI navigation and not in game</li>
<li>Fixed audio latency increasing when switching audio devices</li>
<li>Fixed VDPAU scaling behavior on AMD cards</li>
<li>Fixed several bugs in gamepad navigation behavior</li>
<li>Work around a bug in QGnomePlatform that breaks dialog rendering on Linux</li>
<li>Updated included SDL gamepad mappings</li>
<li>Fixed hotkey behavior on non-latin keyboards</li>
<li>Fixed incorrect frame drop percentages in debug stats</li>
</ul>
</description>
</release>
<release version="0.8.1" date="2019-01-08">
<description>
<p>New features:</p>
<ul>
<li>Stricter adherence to the specified bitrate when streaming over the Internet</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed audio crackling that impacted certain systems</li>
<li>Fixed Wake-on-LAN packets failing to send on some systems</li>
<li>Reduced audio latency when resuming a session where audio is already playing</li>
<li>Added option to enable the old SDL audio renderer by setting ML_AUDIO=SDL</li>
<li>Updated to use the new Qt 5.12 Flatpak runtime</li>
<li>Updated to FFmpeg 4.1</li>
<li>Minor UI tweaks</li>
</ul>
</description>
</release>
<release version="0.8.0" date="2018-12-25">
<description>
<p>New features:</p>
<ul>
<li>Improved security of pairing - After this update, you must re-pair with your PC!</li>
<li>Enabled quitting games started by other Moonlight clients also running this update</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed the video bitrate slider not working above certain values on GFE 3.16</li>
<li>Fixed broken Bluetooth audio with PulseAudio</li>
<li>Reduced audio latency</li>
<li>Fixed some non-gamepad devices triggering the unmapped gamepad dialog box</li>
<li>Minor UI tweaks</li>
</ul>
</description>
</release>
<release version="0.7.0" date="2018-12-05">
<description>
<p>New features:</p>
<ul>
<li>Added option to quit the app after stopping the stream</li>
<li>Added CLI option to quit the running app</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Switched to libsoundio backend to address audio glitches and reduce latency</li>
<li>Fixed CLI version output</li>
<li>Fixed streaming error messages not showing up when launching from CLI</li>
<li>Updated included SDL gamepad mappings</li>
<li>Various minor UI tweaks</li>
</ul>
</description>
</release>
<release version="0.6.4" date="2018-11-20">
<description>
<p>New features:</p>
<ul>
<li>Added an option to match client display refresh rate when FPS is unlocked</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed multiple gamepads not being detected on the host in some scenarios</li>
<li>Fixed an incorrect warning when trying to stream at 4K</li>
</ul>
</description>
</release>
<release version="0.6.3" date="2018-11-16">
<description>
<p>New features:</p>
<ul>
<li>Added support for GeForce Experience 3.16</li>
<li>Added an option to force Moonlight to start in windowed mode</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed scrolling not working on the settings page</li>
<li>Skipped using proxy servers when attempting to stream</li>
<li>Fixed a couple possible crashes</li>
<li>Renamed the mouse acceleration option to be more clear</li>
</ul>
</description>
</release>
<release version="0.6.2" date="2018-10-28">
<description>
<p>New features:</p>
<ul>
<li>Added automatic IP address detection for Internet streaming</li>
<li>Added a quit shortcut tip for gamepad users</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed server state polling not being stopped while streaming</li>
</ul>
</description>
</release>
<release version="0.6.1" date="2018-10-14"> <release version="0.6.1" date="2018-10-14">
<description> <description>
<p>New features:</p> <p>New features:</p>
@@ -172,7 +291,7 @@
</categories> </categories>
<developer_name>Moonlight Game Streaming Team</developer_name> <developer_name>Moonlight Game Streaming Team</developer_name>
<update_contact>cameron@moonlight-stream.com</update_contact> <update_contact>cameron@moonlight-stream.org</update_contact>
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable> <launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
+67 -22
View File
@@ -66,7 +66,7 @@ GridView {
Image { Image {
id: appIcon id: appIcon
anchors.horizontalCenter: parent.horizontalCenter; anchors.horizontalCenter: parent.horizontalCenter
y: 20 y: 20
source: model.boxart source: model.boxart
sourceSize { sourceSize {
@@ -78,21 +78,63 @@ GridView {
fillMode: Image.Pad fillMode: Image.Pad
} }
Image { ToolButton {
id: runningIcon id: resumeButton
anchors.verticalCenterOffset: -50
anchors.centerIn: appIcon anchors.centerIn: appIcon
visible: model.running visible: model.running
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg" source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
anchors.centerIn: parent
sourceSize { sourceSize {
width: 75 width: 75
height: 75 height: 75
} }
} }
Text { onClicked: {
launchOrResumeSelectedApp()
}
ToolTip.text: "Resume Game"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
ToolButton {
id: quitButton
anchors.verticalCenterOffset: 50
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-cancel-24px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
doQuitGame()
}
ToolTip.text: "Quit Game"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
Label {
id: appNameText id: appNameText
text: model.name text: model.name
color: "white"
width: parent.width width: parent.width
height: 125 height: 125
anchors.top: appIcon.bottom anchors.top: appIcon.bottom
@@ -126,28 +168,34 @@ GridView {
} }
onClicked: { onClicked: {
// Nothing is running or this app is running if (model.running) {
launchOrResumeSelectedApp() // This will primarily be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open()
} }
else {
MouseArea { launchOrResumeSelectedApp()
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
// popup() ensures the menu appears under the mouse cursor
appContextMenu.popup()
} }
} }
Keys.onMenuPressed: { Keys.onMenuPressed: {
// We must use open() here so the menu is positioned on if (model.running) {
// the ItemDelegate and not where the mouse cursor is // This will primarily be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open() appContextMenu.open()
} }
}
Menu { function doQuitGame() {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
NavigableMenu {
id: appContextMenu id: appContextMenu
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
text: model.running ? "Resume Game" : "Launch Game" text: model.running ? "Resume Game" : "Launch Game"
onTriggered: { onTriggered: {
appContextMenu.close() appContextMenu.close()
@@ -155,12 +203,9 @@ GridView {
} }
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
text: "Quit Game" text: "Quit Game"
onTriggered: { onTriggered: doQuitGame()
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
visible: model.running visible: model.running
} }
} }
+70
View File
@@ -0,0 +1,70 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
Item {
anchors.fill: parent
function onSearchingComputer() {
stageLabel.text = "Establishing connection to PC..."
}
function onQuittingApp() {
stageLabel.text = "Quitting app..."
}
function onFailure(message) {
errorDialog.text = message
errorDialog.open()
}
// 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
onVisibleChanged: {
if (visible && !launcher.isExecuted()) {
toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer)
launcher.quittingApp.connect(onQuittingApp)
launcher.failed.connect(onFailure)
launcher.execute(ComputerManager)
}
}
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
}
}
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onVisibleChanged: {
if (!visible) {
Qt.quit()
}
}
}
}
+48 -2
View File
@@ -1,3 +1,4 @@
import QtQml 2.2
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2 import QtQuick.Dialogs 1.2
@@ -30,13 +31,19 @@ Item {
errorDialog.open() errorDialog.open()
} }
function onAppQuitRequired(appName) {
quitAppDialog.appName = appName
quitAppDialog.open()
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible && !launcher.isExecuted()) {
toolBar.visible = false toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer) launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp) launcher.searchingApp.connect(onSearchingApp)
launcher.sessionCreated.connect(onSessionCreated) launcher.sessionCreated.connect(onSessionCreated)
launcher.failed.connect(onLaunchFailed) launcher.failed.connect(onLaunchFailed)
launcher.appQuitRequired.connect(onAppQuitRequired)
launcher.execute(ComputerManager) launcher.execute(ComputerManager)
} }
} }
@@ -56,7 +63,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
} }
@@ -75,4 +81,44 @@ Item {
} }
} }
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
property string appName : ""
function quitApp() {
var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName}
stackView.push(component.createObject(stackView, params))
// Trigger the quit after pushing the quit segue on screen
launcher.quitRunningApp()
}
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp()
// Exit process if app quit is rejected (reacts also to closing of the
// dialog from title bar's close button).
// Note: this depends on undocumented behavior of visibleChanged()
// signal being emitted before yes() or accepted() has been emitted.
onVisibleChanged: {
if (!visible) {
quitTimer.start()
}
}
Component.onCompleted: {
yes.connect(quitTimer.stop)
accepted.connect(quitTimer.stop)
}
}
Timer {
id: quitTimer
interval: 100
onTriggered: Qt.quit()
}
} }
+8
View File
@@ -0,0 +1,8 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
Menu {
onOpened: {
itemAt(0).forceActiveFocus(Qt.TabFocusReason)
}
}
+9
View File
@@ -2,11 +2,20 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
MenuItem { MenuItem {
// Qt 5.10 has a menu property, but we need to support 5.9
// so we must make our own.
property Menu parentMenu
// Ensure focus can't be given to an invisible item // Ensure focus can't be given to an invisible item
enabled: visible enabled: visible
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
Keys.onReturnPressed: { Keys.onReturnPressed: {
triggered() triggered()
} }
Keys.onEscapePressed: {
parentMenu.close()
}
} }
+23 -12
View File
@@ -124,10 +124,9 @@ GridView {
visible: !model.addPc && model.statusUnknown visible: !model.addPc && model.statusUnknown
} }
Text { Label {
id: pcNameText id: pcNameText
text: model.name text: model.name
color: "white"
width: parent.width width: parent.width
anchors.top: pcIcon.bottom anchors.top: pcIcon.bottom
@@ -136,14 +135,10 @@ GridView {
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
Menu { NavigableMenu {
id: pcContextMenu id: pcContextMenu
NavigableMenuItem { NavigableMenuItem {
text: "Wake PC" parentMenu: pcContextMenu
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
}
NavigableMenuItem {
text: "Delete PC" text: "Delete PC"
onTriggered: { onTriggered: {
deletePcDialog.pcIndex = index deletePcDialog.pcIndex = index
@@ -151,6 +146,12 @@ GridView {
deletePcDialog.open() deletePcDialog.open()
} }
} }
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
}
} }
onClicked: { onClicked: {
@@ -187,14 +188,24 @@ GridView {
} }
} }
onPressAndHold: {
if (!model.addPc) {
// popup() ensures the menu appears under the mouse cursor
if (pcContextMenu.popup) {
pcContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
pcContextMenu.open()
}
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton; acceptedButtons: Qt.RightButton;
onClicked: { onClicked: {
if (!model.addPc) { parent.onPressAndHold()
// popup() ensures the menu appears under the mouse cursor
pcContextMenu.popup()
}
} }
} }
+11 -5
View File
@@ -27,19 +27,26 @@ Item {
errorDialog.open() errorDialog.open()
} }
// Exit this view
stackView.pop()
// If we're supposed to launch another game after this, do so now // If we're supposed to launch another game after this, do so now
if (error === undefined && nextSession !== null) { if (error === undefined && nextSession !== null) {
var component = Qt.createComponent("StreamSegue.qml") var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession}) var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
stackView.push(segue) stackView.replace(segue)
}
else {
// Show the toolbar again
toolBar.visible = true
// Exit this view
stackView.pop()
} }
} }
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
// Hide the toolbar before we start loading
toolBar.visible = false
// Connect the quit completion signal // Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted) ComputerManager.quitAppCompleted.connect(quitAppCompleted)
} }
@@ -65,7 +72,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
} }
+115 -29
View File
@@ -5,9 +5,24 @@ import StreamingPreferences 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
ScrollView { Flickable {
id: settingsPage id: settingsPage
objectName: "Settings" objectName: "Settings"
anchors.fill: parent
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
ScrollBar.vertical: ScrollBar {
parent: settingsPage.parent
anchors {
top: settingsPage.top
left: settingsPage.right
bottom: settingsPage.bottom
leftMargin: -10
}
}
StreamingPreferences { StreamingPreferences {
id: prefs id: prefs
@@ -58,16 +73,14 @@ ScrollView {
text: qsTr("Resolution and FPS") text: qsTr("Resolution and FPS")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Label { Label {
width: parent.width width: parent.width
id: resFPSdesc id: resFPSdesc
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors.") text: qsTr("Setting values too high for your PC or network connection may cause lag, stuttering, or errors.")
font.pointSize: 9 font.pointSize: 9
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Row { Row {
@@ -211,8 +224,17 @@ ScrollView {
// Use 64 as the cutoff for adding a separate option to // Use 64 as the cutoff for adding a separate option to
// handle wonky displays that report just over 60 Hz. // handle wonky displays that report just over 60 Hz.
if (max_fps > 64) { if (max_fps > 64) {
// Mark any FPS value greater than 120 as unsupported
if (prefs.unsupportedFps && max_fps > 120) {
fpsListModel.append({"text": max_fps+" FPS (Unsupported)", "video_fps": ""+max_fps})
}
else if (max_fps > 120) {
fpsListModel.append({"text": "120 FPS", "video_fps": "120"})
}
else {
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps}) fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
} }
}
// Add unsupported FPS values that come after the display max FPS // Add unsupported FPS values that come after the display max FPS
if (prefs.unsupportedFps) { if (prefs.unsupportedFps) {
@@ -273,21 +295,18 @@ ScrollView {
text: qsTr("Video bitrate: ") text: qsTr("Video bitrate: ")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Label { Label {
width: parent.width width: parent.width
id: bitrateDesc id: bitrateDesc
text: qsTr("Lower bitrate to reduce lag and stuttering. Raise bitrate to increase image quality.") text: qsTr("Lower the bitrate on slower connections. Raise the bitrate to increase image quality.")
font.pointSize: 9 font.pointSize: 9
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Slider { Slider {
id: slider id: slider
wheelEnabled: true
value: prefs.bitrateKbps value: prefs.bitrateKbps
@@ -310,7 +329,6 @@ ScrollView {
text: qsTr("Display mode") text: qsTr("Display mode")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -329,6 +347,7 @@ ScrollView {
} }
id: windowModeComboBox id: windowModeComboBox
hoverEnabled: true
textRole: "text" textRole: "text"
model: ListModel { model: ListModel {
id: windowModeListModel id: windowModeListModel
@@ -348,16 +367,43 @@ ScrollView {
onActivated: { onActivated: {
prefs.windowMode = windowModeListModel.get(currentIndex).val prefs.windowMode = windowModeListModel.get(currentIndex).val
} }
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Full-screen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc."
} }
CheckBox { CheckBox {
id: vsyncCheck id: vsyncCheck
text: "<font color=\"white\">Enable V-Sync</font>" hoverEnabled: true
text: "V-Sync"
font.pointSize: 12 font.pointSize: 12
checked: prefs.enableVsync checked: prefs.enableVsync
onCheckedChanged: { onCheckedChanged: {
prefs.enableVsync = checked prefs.enableVsync = checked
} }
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing"
}
CheckBox {
id: framePacingCheck
hoverEnabled: true
text: "Frame pacing"
font.pointSize: 12
enabled: prefs.enableVsync
checked: prefs.enableVsync && prefs.framePacing
onCheckedChanged: {
prefs.framePacing = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Frame pacing reduces micro-stutter by delaying frames that come in too early"
} }
} }
} }
@@ -380,7 +426,6 @@ ScrollView {
text: qsTr("Audio configuration") text: qsTr("Audio configuration")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -419,6 +464,29 @@ ScrollView {
} }
} }
GroupBox {
id: uiSettingsGroupBox
width: (parent.width - 2 * parent.padding)
padding: 12
title: "<font color=\"skyblue\">UI Settings</font>"
font.pointSize: 12
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: startMaximizedCheck
text: "Maximize Moonlight window on startup"
font.pointSize: 12
checked: !prefs.startWindowed
onCheckedChanged: {
prefs.startWindowed = !checked
}
}
}
}
} }
Column { Column {
@@ -434,28 +502,40 @@ ScrollView {
title: "<font color=\"skyblue\">Input Settings</font>" title: "<font color=\"skyblue\">Input Settings</font>"
font.pointSize: 12 font.pointSize: 12
Row { Column {
anchors.fill: parent anchors.fill: parent
spacing: 5 spacing: 5
CheckBox { CheckBox {
id: multiControllerCheck id: singleControllerCheck
text: "<font color=\"white\">Multiple controller support</font>" text: "Force gamepad #1 always present"
font.pointSize: 12 font.pointSize: 12
checked: prefs.multiController checked: !prefs.multiController
onCheckedChanged: { onCheckedChanged: {
prefs.multiController = checked prefs.multiController = !checked
} }
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.\n" +
"Only enable this option when streaming a game that doesn't support gamepads being connected after startup."
} }
CheckBox { CheckBox {
id: mouseAccelerationCheck id: rawInputCheck
text: "<font color=\"white\">Enable mouse acceleration</font>" hoverEnabled: true
text: "Raw mouse input"
font.pointSize: 12 font.pointSize: 12
checked: prefs.mouseAcceleration checked: !prefs.mouseAcceleration
onCheckedChanged: { onCheckedChanged: {
prefs.mouseAcceleration = checked prefs.mouseAcceleration = !checked
} }
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "When checked, mouse input is not accelerated or scaled by the OS before passing to Moonlight"
} }
} }
} }
@@ -467,15 +547,13 @@ ScrollView {
title: "<font color=\"skyblue\">Host Settings</font>" title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12 font.pointSize: 12
Row { Column {
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: "Optimize game settings"
// 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: {
@@ -485,7 +563,7 @@ ScrollView {
CheckBox { CheckBox {
id: audioPcCheck id: audioPcCheck
text: "<font color=\"white\">Play audio on host PC</font>" text: "Play audio on host PC"
font.pointSize: 12 font.pointSize: 12
checked: prefs.playAudioOnHost checked: prefs.playAudioOnHost
onCheckedChanged: { onCheckedChanged: {
@@ -512,7 +590,6 @@ ScrollView {
text: qsTr("Video decoder") text: qsTr("Video decoder")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -559,7 +636,6 @@ ScrollView {
text: qsTr("Video codec") text: qsTr("Video codec")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -609,7 +685,7 @@ ScrollView {
CheckBox { CheckBox {
id: unlockUnsupportedFps id: unlockUnsupportedFps
text: "<font color=\"white\">Unlock unsupported FPS options</font>" text: "Unlock unsupported FPS options"
font.pointSize: 12 font.pointSize: 12
checked: prefs.unsupportedFps checked: prefs.unsupportedFps
onCheckedChanged: { onCheckedChanged: {
@@ -623,7 +699,7 @@ ScrollView {
CheckBox { CheckBox {
id: enableMdns id: enableMdns
text: "<font color=\"white\">Automatically find PCs on the local network (Recommended)</font>" text: "Automatically find PCs on the local network (Recommended)"
font.pointSize: 12 font.pointSize: 12
checked: prefs.enableMdns checked: prefs.enableMdns
onCheckedChanged: { onCheckedChanged: {
@@ -640,6 +716,16 @@ ScrollView {
} }
} }
} }
CheckBox {
id: quitAppAfter
text: "Quit app after quitting session"
font.pointSize: 12
checked: prefs.quitAppAfter
onCheckedChanged: {
prefs.quitAppAfter = checked
}
}
} }
} }
} }
+70 -22
View File
@@ -3,6 +3,8 @@ import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2 import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
import Session 1.0 import Session 1.0
Item { Item {
@@ -58,30 +60,34 @@ Item {
toast.visible = true toast.visible = true
} }
onVisibleChanged: { function quitStarting()
if (visible) { {
// Hide the toolbar before we start loading // Avoid the push transition animation
toolBar.visible = false var component = Qt.createComponent("QuitSegue.qml")
stackView.replace(stackView.currentItem, component.createObject(stackView, {"appName": appName}), StackView.Immediate)
// Hook up our signals // Show the Qt window again to show quit segue
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
// Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY)
if (quitAfter) {
Qt.quit()
} else {
// Show the Qt window again after streaming
window.visible = true window.visible = true
}
function sessionFinished()
{
if (quitAfter) {
if (errorDialog.text) {
// Quit when the error dialog is acknowledged
errorDialog.open()
}
else {
// Quit immediately
Qt.quit()
}
} else {
// Exit this view // Exit this view
stackView.pop() stackView.pop()
// Show the Qt window again after streaming
window.visible = true
// Display any launch errors. We do this after // Display any launch errors. We do this after
// the Qt UI is visible again to prevent losing // the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad // focus on the dialog which would impact gamepad
@@ -91,7 +97,40 @@ Item {
} }
} }
} }
else {
// It's important that we don't call enable() here
// or it may interfere with the Session instance
// getting notified of initial connected gamepads.
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
// Hide the toolbar before we start loading
toolBar.visible = false
// Set the hint text. We do this here rather than
// in the hintText control itself to synchronize
// with Session.exec() which requires no concurrent
// gamepad usage.
hintText.text = gamepadKeyNav.getConnectedGamepads() > 0 ?
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
// Hook up our signals
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY)
}
else if (!quitAfter) {
// Show the toolbar again when we become hidden // Show the toolbar again when we become hidden
toolBar.visible = true toolBar.visible = true
} }
@@ -113,12 +152,11 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
} }
Label { Label {
text: "Tip: Press Ctrl+Alt+Shift+Q to disconnect your session" id: hintText
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 50 anchors.bottomMargin: 50
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -126,7 +164,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
MessageDialog { MessageDialog {
@@ -134,8 +171,19 @@ Item {
modality:Qt.WindowModal modality:Qt.WindowModal
icon: StandardIcon.Critical icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help standardButtons: StandardButton.Ok | StandardButton.Help
onAccepted: {
if (quitAfter) {
Qt.quit()
}
}
onHelp: { onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"); Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
if (quitAfter) {
Qt.quit()
}
} }
} }
} }
+31 -14
View File
@@ -4,8 +4,6 @@ import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls.Material 2.1
import ComputerManager 1.0 import ComputerManager 1.0
import AutoUpdateChecker 1.0 import AutoUpdateChecker 1.0
import StreamingPreferences 1.0 import StreamingPreferences 1.0
@@ -18,14 +16,7 @@ ApplicationWindow {
width: 1280 width: 1280
height: 600 height: 600
// Maximize the window by default when the stream is configured visibility: prefs.startWindowed ? "Windowed" : "Maximized"
// for full-screen or borderless windowed. This is ideal for TV
// setups where the user doesn't want a tiny window in the middle
// of their screen when starting Moonlight.
visibility: prefs.windowMode != StreamingPreferences.WM_WINDOWED ? "Maximized" : "Windowed"
Material.theme: Material.Dark
Material.accent: Material.Purple
StreamingPreferences { StreamingPreferences {
id: prefs id: prefs
@@ -89,6 +80,29 @@ ApplicationWindow {
} }
} }
onVisibleChanged: {
// When we become invisible while streaming is going on,
// stop polling immediately.
if (!visible) {
inactivityTimer.stop()
if (pollingActive) {
ComputerManager.stopPollingAsync()
pollingActive = false
}
}
else if (active) {
// When we become visible and active again, start polling
inactivityTimer.stop()
// Restart polling if it was stopped
if (!pollingActive) {
ComputerManager.startPolling()
pollingActive = true
}
}
}
onActiveChanged: { onActiveChanged: {
if (active) { if (active) {
// Stop the inactivity timer // Stop the inactivity timer
@@ -118,6 +132,11 @@ ApplicationWindow {
// Doing it earlier can lead to the dialog appearing behind // Doing it earlier can lead to the dialog appearing behind
// the window or otherwise without input focus. // the window or otherwise without input focus.
if (!initialized) { if (!initialized) {
// Set initialized before calling anything else, because
// pumping the event loop can cause us to get another
// onAfterRendering call and potentially reenter this code.
initialized = true;
if (prefs.isRunningWayland()) { if (prefs.isRunningWayland()) {
waylandDialog.open() waylandDialog.open()
} }
@@ -133,8 +152,6 @@ ApplicationWindow {
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
unmappedGamepadDialog.open() unmappedGamepadDialog.open()
} }
initialized = true;
} }
} }
@@ -213,7 +230,6 @@ ApplicationWindow {
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: "Update available for Moonlight"
// Invisible until we get a callback notifying us that // Invisible until we get a callback notifying us that
// an update is available // an update is available
@@ -221,8 +237,9 @@ ApplicationWindow {
onClicked: Qt.openUrlExternally(browserUrl); onClicked: Qt.openUrlExternally(browserUrl);
function updateAvailable(url) function updateAvailable(version, url)
{ {
ToolTip.text = "Update available for Moonlight: Version " + version
updateButton.browserUrl = url updateButton.browserUrl = url
updateButton.visible = true updateButton.visible = true
} }
+25
View File
@@ -244,3 +244,28 @@ void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
{ {
m_SettingsMode = settingsMode; m_SettingsMode = settingsMode;
} }
int SdlGamepadKeyNavigation::getConnectedGamepads()
{
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
return 0;
}
// Applying mappings is necessary to ensure gamepad without
// a built-in mapping are properly counted.
MappingManager mappingManager;
mappingManager.applyMappings();
int count = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
count++;
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return count;
}
+2
View File
@@ -20,6 +20,8 @@ public:
Q_INVOKABLE void setSettingsMode(bool settingsMode); Q_INVOKABLE void setSettingsMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads();
private: private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier); void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
+58 -3
View File
@@ -5,6 +5,7 @@
#include <QQuickStyle> #include <QQuickStyle>
#include <QMutex> #include <QMutex>
#include <QtDebug> #include <QtDebug>
#include <QNetworkProxyFactory>
// 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
@@ -20,6 +21,7 @@
#include "antihookingprotection.h" #include "antihookingprotection.h"
#endif #endif
#include "cli/quitstream.h"
#include "cli/startstream.h" #include "cli/startstream.h"
#include "cli/commandlineparser.h" #include "cli/commandlineparser.h"
#include "path.h" #include "path.h"
@@ -134,6 +136,14 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
break; break;
} }
// HACK: Avoid printing this internal Qt warning which seems to print every time we
// start our event loop for the QNetworkReply. I have found this warning in many
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
// new in Qt 5.12.
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
return;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed()); QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg); QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
@@ -221,6 +231,9 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
// Set the app version for the QCommandLineParser's showVersion() command
QCoreApplication::setApplicationVersion(VERSION_STR);
// Set these here to allow us to use the default QSettings constructor. // Set these here to allow us to use the default QSettings constructor.
// These also ensure that our cache directory is named correctly. As such, // These also ensure that our cache directory is named correctly. As such,
// it is critical that these be called before Path::initialize(). // it is critical that these be called before Path::initialize().
@@ -277,7 +290,7 @@ int main(int argc, char *argv[])
// 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", "1");
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
if (!qEnvironmentVariableIsSet("QT_OPENGL")) { if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
@@ -288,9 +301,20 @@ int main(int argc, char *argv[])
} }
#endif #endif
// We don't want system proxies to apply to us
QNetworkProxyFactory::setUseSystemConfiguration(false);
// Clear any default application proxy
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
QNetworkProxy::setApplicationProxy(noProxy);
// Register custom metatypes for use in signals // Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp"); qRegisterMetaType<NvApp>("NvApp");
// Disable desktop settings awareness due to issues with QGnomePlatform
// breaking our dialog rendering on Linux.
QGuiApplication::setDesktopSettingsAware(false);
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
app.setWindowIcon(QIcon(":/res/moonlight.svg")); app.setWindowIcon(QIcon(":/res/moonlight.svg"));
@@ -312,7 +336,25 @@ int main(int argc, char *argv[])
return new AutoUpdateChecker(); return new AutoUpdateChecker();
}); });
QQuickStyle::setStyle("Material"); // Use the dense material dark theme by default
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_1_STYLE")) {
qputenv("QT_QUICK_CONTROLS_1_STYLE", "Base");
}
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
QString initialView; QString initialView;
@@ -334,6 +376,15 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty("launcher", launcher); engine.rootContext()->setContextProperty("launcher", launcher);
break; break;
} }
case GlobalCommandLineParser::QuitRequested:
{
initialView = "CliQuitStreamSegue.qml";
QuitCommandLineParser quitParser;
quitParser.parse(app.arguments());
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
engine.rootContext()->setContextProperty("launcher", launcher);
break;
}
} }
engine.rootContext()->setContextProperty("initialView", initialView); engine.rootContext()->setContextProperty("initialView", initialView);
@@ -344,7 +395,7 @@ int main(int argc, char *argv[])
return -1; return -1;
SDL_SetMainReady(); SDL_SetMainReady();
if (SDL_Init(0) != 0) { if (SDL_Init(SDL_INIT_TIMER) != 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());
@@ -361,5 +412,9 @@ int main(int argc, char *argv[])
int err = app.exec(); int err = app.exec();
// Give worker tasks time to properly exit. Fixes PendingQuitTask
// sometimes freezing and blocking process exit.
QThreadPool::globalInstance()->waitForDone(30000);
return err; return err;
} }
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

+6 -7
View File
@@ -8,7 +8,6 @@
QString Path::s_LogDir; QString Path::s_LogDir;
QString Path::s_BoxArtCacheDir; QString Path::s_BoxArtCacheDir;
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
QString Path::getLogDir() QString Path::getLogDir()
{ {
@@ -22,24 +21,24 @@ QString Path::getBoxArtCacheDir()
return s_BoxArtCacheDir; return s_BoxArtCacheDir;
} }
QString Path::getGamepadMappingFile() QString Path::getDataFilePath(QString fileName)
{ {
QString candidatePath; QString candidatePath;
// Check the current directory first // Check the current directory first
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile); candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) { if (QFile::exists(candidatePath)) {
return candidatePath; return candidatePath;
} }
// Now check the data directories (for Linux, in particular) // Now check the data directories (for Linux, in particular)
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile); candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, fileName);
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) { if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
return candidatePath; return candidatePath;
} }
// Now try the directory of our app installation (for Windows, if current dir doesn't find it) // Now try the directory of our app installation (for Windows, if current dir doesn't find it)
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile); candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) { if (QFile::exists(candidatePath)) {
return candidatePath; return candidatePath;
} }
@@ -48,8 +47,8 @@ QString Path::getGamepadMappingFile()
QDir dir = QDir(QCoreApplication::applicationDirPath()); QDir dir = QDir(QCoreApplication::applicationDirPath());
dir.cdUp(); dir.cdUp();
dir.cd("Resources"); dir.cd("Resources");
dir.filePath(k_GameControllerMappingFile); dir.filePath(fileName);
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile); candidatePath = dir.absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) { if (QFile::exists(candidatePath)) {
return candidatePath; return candidatePath;
} }
+1 -2
View File
@@ -9,12 +9,11 @@ public:
static QString getBoxArtCacheDir(); static QString getBoxArtCacheDir();
static QString getGamepadMappingFile(); static QString getDataFilePath(QString fileName);
static void initialize(bool portable); static void initialize(bool portable);
private: private:
static QString s_LogDir; static QString s_LogDir;
static QString s_BoxArtCacheDir; static QString s_BoxArtCacheDir;
static const QString k_GameControllerMappingFile;
}; };
+2
View File
@@ -10,7 +10,9 @@
<file>gui/NavigableToolButton.qml</file> <file>gui/NavigableToolButton.qml</file>
<file>gui/NavigableItemDelegate.qml</file> <file>gui/NavigableItemDelegate.qml</file>
<file>gui/NavigableMenuItem.qml</file> <file>gui/NavigableMenuItem.qml</file>
<file>gui/CliQuitStreamSegue.qml</file>
<file>gui/CliStartStreamSegue.qml</file> <file>gui/CliStartStreamSegue.qml</file>
<file>gui/AutoResizingComboBox.qml</file> <file>gui/AutoResizingComboBox.qml</file>
<file>gui/NavigableMenu.qml</file>
</qresource> </qresource>
</RCC> </RCC>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 325 B

+1 -69
View File
@@ -1,69 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><g><circle vector-effect="non-scaling-stroke" cx="128" cy="128" r="128" fill="rgb(86,92,100)"/><circle vector-effect="non-scaling-stroke" cx="128.00000000000003" cy="128.00000000000003" r="96" fill="rgb(255,255,255)"/><path d=" M 137.66 132 L 224 132 L 224 124 L 137.66 124 L 198.71 62.95 L 193.05 57.29 L 132 118.34 L 132 32 L 124 32 L 124 118.34 L 62.95 57.29 L 57.29 62.95 L 118.34 124 L 32 124 L 32 132 L 118.34 132 L 57.29 193.05 L 62.95 198.71 L 124 137.66 L 124 224 L 132 224 L 132 137.66 L 193.05 198.71 L 198.71 193.05 L 137.66 132 Z " fill-rule="evenodd" fill="rgb(86,92,100)"/></g></svg>
<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"
id="svg3761"
viewBox="0 0 680 680"
height="680"
width="680"
version="1.0">
<metadata
id="metadata3767">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<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>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 820 B

+1 -1
View File
@@ -1,3 +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"> <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="#FF0000">
<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" /> <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> </svg>

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 499 B

+1
View File
@@ -8,6 +8,7 @@
<file>res/baseline-lock-24px.svg</file> <file>res/baseline-lock-24px.svg</file>
<file>res/baseline-play_circle_filled_white-48px.svg</file> <file>res/baseline-play_circle_filled_white-48px.svg</file>
<file>res/baseline-warning-24px.svg</file> <file>res/baseline-warning-24px.svg</file>
<file>res/baseline-cancel-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>
+1 -1
View File
@@ -54,7 +54,7 @@ void MappingManager::save()
void MappingManager::applyMappings() void MappingManager::applyMappings()
{ {
QString mappingFile = Path::getGamepadMappingFile(); QString mappingFile = Path::getDataFilePath("gamecontrollerdb.txt");
if (!mappingFile.isEmpty()) { if (!mappingFile.isEmpty()) {
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString(); std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
+10 -3
View File
@@ -20,7 +20,10 @@
#define SER_WINDOWMODE "windowmode" #define SER_WINDOWMODE "windowmode"
#define SER_UNSUPPORTEDFPS "unsupportedfps" #define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns" #define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter"
#define SER_MOUSEACCELERATION "mouseacceleration" #define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
StreamingPreferences::StreamingPreferences(QObject *parent) StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent) : QObject(parent)
@@ -42,7 +45,10 @@ void StreamingPreferences::reload()
multiController = settings.value(SER_MULTICONT, true).toBool(); multiController = settings.value(SER_MULTICONT, true).toBool();
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool(); unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool(); enableMdns = settings.value(SER_MDNS, true).toBool();
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool(); mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG, audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt()); static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG, videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -69,7 +75,10 @@ void StreamingPreferences::save()
settings.setValue(SER_MULTICONT, multiController); settings.setValue(SER_MULTICONT, multiController);
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps); settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns); settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration); settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_STARTWINDOWED, startWindowed);
settings.setValue(SER_FRAMEPACING, framePacing);
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));
@@ -133,9 +142,7 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
} }
} }
// Cap the frame rate at 120 FPS. Past this, the encoders start maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
// to max out and drop frames.
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
} }
} }
+9
View File
@@ -73,7 +73,10 @@ public:
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 unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged) Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged) Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
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)
@@ -90,7 +93,10 @@ public:
bool multiController; bool multiController;
bool unsupportedFps; bool unsupportedFps;
bool enableMdns; bool enableMdns;
bool quitAppAfter;
bool mouseAcceleration; bool mouseAcceleration;
bool startWindowed;
bool framePacing;
AudioConfig audioConfig; AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig; VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection; VideoDecoderSelection videoDecoderSelection;
@@ -105,10 +111,13 @@ signals:
void multiControllerChanged(); void multiControllerChanged();
void unsupportedFpsChanged(); void unsupportedFpsChanged();
void enableMdnsChanged(); void enableMdnsChanged();
void quitAppAfterChanged();
void mouseAccelerationChanged(); void mouseAccelerationChanged();
void audioConfigChanged(); void audioConfigChanged();
void videoCodecConfigChanged(); void videoCodecConfigChanged();
void videoDecoderSelectionChanged(); void videoDecoderSelectionChanged();
void windowModeChanged(); void windowModeChanged();
void startWindowedChanged();
void framePacingChanged();
}; };
+33 -2
View File
@@ -3,15 +3,19 @@
#ifdef HAVE_SOUNDIO #ifdef HAVE_SOUNDIO
#include "renderers/soundioaudiorenderer.h" #include "renderers/soundioaudiorenderer.h"
#else
#include "renderers/sdl.h"
#endif #endif
#include "renderers/sdl.h"
#include <Limelight.h> #include <Limelight.h>
IAudioRenderer* Session::createAudioRenderer() IAudioRenderer* Session::createAudioRenderer()
{ {
#ifdef HAVE_SOUNDIO #ifdef HAVE_SOUNDIO
if (qgetenv("ML_AUDIO") == "SDL") {
return new SdlAudioRenderer();
}
return new SoundIoAudioRenderer(); return new SoundIoAudioRenderer();
#else #else
return new SdlAudioRenderer(); return new SdlAudioRenderer();
@@ -111,6 +115,21 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
} }
} }
// See if we need to drop this sample
if (s_ActiveSession->m_DropAudioEndTime != 0) {
if (SDL_TICKS_PASSED(SDL_GetTicks(), s_ActiveSession->m_DropAudioEndTime)) {
// Avoid calling SDL_GetTicks() now
s_ActiveSession->m_DropAudioEndTime = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio drop window has ended");
}
else {
// We're still in the drop window
return;
}
}
s_ActiveSession->m_AudioSampleCount++; s_ActiveSession->m_AudioSampleCount++;
if (s_ActiveSession->m_AudioRenderer != nullptr) { if (s_ActiveSession->m_AudioRenderer != nullptr) {
@@ -141,10 +160,22 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
// safe to reinitialize here because we can't be torn down while // safe to reinitialize here because we can't be torn down while
// the audio decoder/playback thread is still alive. // the audio decoder/playback thread is still alive.
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) { if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
// Since we're doing this inline and audio initialization takes time, we need
// to drop samples to account for the time we've spent blocking audio rendering
// so we return to real-time playback and don't accumulate latency.
Uint32 audioReinitStartTime = SDL_GetTicks();
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(); s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) { if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
delete s_ActiveSession->m_AudioRenderer; delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr; s_ActiveSession->m_AudioRenderer = nullptr;
} }
Uint32 audioReinitStopTime = SDL_GetTicks();
s_ActiveSession->m_DropAudioEndTime = audioReinitStopTime + (audioReinitStopTime - audioReinitStartTime);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio reinitialization took %d ms - starting drop window",
audioReinitStopTime - audioReinitStartTime);
} }
} }
-5
View File
@@ -16,9 +16,4 @@ public:
private: private:
SDL_AudioDeviceID m_AudioDevice; SDL_AudioDeviceID m_AudioDevice;
Uint32 m_ChannelCount;
Uint32 m_PendingDrops;
Uint32 m_PendingHardDrops;
Uint32 m_SampleIndex;
Uint32 m_BaselinePendingData;
}; };
+1 -79
View File
@@ -5,18 +5,8 @@
#include <QtGlobal> #include <QtGlobal>
#define MIN_QUEUED_FRAMES 2
#define MAX_QUEUED_FRAMES 4
#define STOP_THE_WORLD_LIMIT 20
#define DROP_RATIO_DENOM 32
SdlAudioRenderer::SdlAudioRenderer() SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0), : 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)); SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
@@ -61,27 +51,6 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
have.samples, have.samples,
have.size); 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 // Start playback
SDL_PauseAudioDevice(m_AudioDevice, 0); SDL_PauseAudioDevice(m_AudioDevice, 0);
@@ -102,53 +71,6 @@ SdlAudioRenderer::~SdlAudioRenderer()
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize) bool 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 true;
}
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 true;
}
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) { if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s", "Failed to queue audio sample: %s",
@@ -4,12 +4,30 @@
#include <QtGlobal> #include <QtGlobal>
// GFE sends us packets in 5 ms chunks
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA require more than just 5 ms samples
// for some reason, so write a minimum of 25 ms each time to
// prevent underruns on Bluetooth.
// https://github.com/moonlight-stream/moonlight-qt/issues/147
// https://github.com/moonlight-stream/moonlight-qt/issues/157
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.025;
#else
// This determines the size of the buffers we'll
// get from CoreAudio. It is also the minimum
// size that we will write when called to fill a buffer.
const double SoundIoAudioRenderer::k_MinSampleLengthSec = k_RawSampleLengthSec;
#endif
SoundIoAudioRenderer::SoundIoAudioRenderer() SoundIoAudioRenderer::SoundIoAudioRenderer()
: m_OpusChannelCount(0), : m_OpusChannelCount(0),
m_SoundIo(nullptr), m_SoundIo(nullptr),
m_Device(nullptr), m_Device(nullptr),
m_OutputStream(nullptr), m_OutputStream(nullptr),
m_RingBuffer(nullptr), m_RingBuffer(nullptr),
m_Latency(0),
m_Errored(false) m_Errored(false)
{ {
@@ -17,6 +35,10 @@ SoundIoAudioRenderer::SoundIoAudioRenderer()
SoundIoAudioRenderer::~SoundIoAudioRenderer() SoundIoAudioRenderer::~SoundIoAudioRenderer()
{ {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio latency: %f",
m_Latency);
if (m_OutputStream != nullptr) { if (m_OutputStream != nullptr) {
soundio_outstream_destroy(m_OutputStream); soundio_outstream_destroy(m_OutputStream);
} }
@@ -152,17 +174,12 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
m_OutputStream->format = SoundIoFormatS16NE; m_OutputStream->format = SoundIoFormatS16NE;
m_OutputStream->sample_rate = opusConfig->sampleRate; m_OutputStream->sample_rate = opusConfig->sampleRate;
m_OutputStream->software_latency = k_MinSampleLengthSec;
m_OutputStream->name = "Moonlight"; m_OutputStream->name = "Moonlight";
m_OutputStream->userdata = this; m_OutputStream->userdata = this;
m_OutputStream->error_callback = sioErrorCallback; m_OutputStream->error_callback = sioErrorCallback;
m_OutputStream->write_callback = sioWriteCallback; m_OutputStream->write_callback = sioWriteCallback;
// This determines the size of the buffers we'll
// get from CoreAudio. Since GFE sends us packets
// in 5 ms chunks, we'll give them to the OS in
// buffers of the same size.
m_OutputStream->software_latency = 0.005;
SoundIoChannelLayout bestLayout = m_Device->current_layout; SoundIoChannelLayout bestLayout = m_Device->current_layout;
for (int i = 0; i < m_Device->layout_count; i++) { for (int i = 0; i < m_Device->layout_count; i++) {
if (scoreChannelLayout(&bestLayout, opusConfig) < if (scoreChannelLayout(&bestLayout, opusConfig) <
@@ -218,12 +235,33 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
} }
} }
// Buffer up to 6 packets of audio (30 ms) to smooth int packetsToBuffer;
// out network packet delivery jitter
#ifdef Q_OS_LINUX
// PulseAudio and ALSA need the large buffer (see comment on k_MinSampleLengthSec),
// so we need a buffer at least double that size to allow packets to arrive
// while we're writing to the sink.
packetsToBuffer = (int)(k_MinSampleLengthSec / k_RawSampleLengthSec) * 2;
#else
if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
// 15 ms buffer seems to be fine for WASAPI
packetsToBuffer = 3;
}
else {
// 30 ms buffer on CoreAudio to avoid glitching on macOS
packetsToBuffer = 6;
}
#endif
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio buffer size: %d packets",
packetsToBuffer);
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo, m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
m_OutputStream->bytes_per_sample * m_OutputStream->bytes_per_sample *
m_OpusChannelCount * m_OpusChannelCount *
SAMPLES_PER_FRAME * 6); SAMPLES_PER_FRAME *
packetsToBuffer);
if (m_RingBuffer == nullptr) { if (m_RingBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_ring_buffer_create() failed"); "soundio_ring_buffer_create() failed");
@@ -238,6 +276,11 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return false; return false;
} }
// HACK: For some reason, a constant latency hangs around in the audio pipeline
// unless we wait for the audio stream to drain before actually submitting any samples.
// This is a gross hack, but it works remarkably well.
SDL_Delay(500);
return true; return true;
} }
@@ -331,31 +374,15 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
// Clamp framesLeft to frameCountMax // Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax); framesLeft = qMin(framesLeft, frameCountMax);
// Place an upper-bound on audio stream latency to // Ensure we always write at least a buffer, even if it's silence, to avoid
// avoid accumulating packets in queue-based backends // busy looping when no audio data is available while libsoundio tries to keep
// like WASAPI. This bound was set by testing on several // us from starving the output device.
// Windows machines. The highest latency was found on frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
// a XPS 9343 running Windows 7 in Steam Big Picture frameCountMin = qMin(frameCountMin, frameCountMax);
// and the 5.1 audio test clip from Fraunhofer.
if (me->m_SoundIo->current_backend == SoundIoBackendWasapi) {
double latency;
if (soundio_outstream_get_latency(stream, &latency) == SoundIoErrorNone) {
if (latency > 0.050) {
// If our latency is higher than desired, drop these samples to gracefully lower
// the latency without glitching too much. Dropping the whole buffer causes
// a much more noticeable glitch. This approach also ensures that we don't
// accidentally underflow if the driver/kernel side is delayed and isn't
// consuming data fast enough. Dropping a frame at a time and re-evaluating
// each time ensures that we'll stop dropping if latency returns to normal.
readPtr += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
bytesRead += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
framesLeft = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, // Track latency on queueing-based backends
"Latency exceeded drop cap: %f", if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
latency); soundio_outstream_get_latency(stream, &me->m_Latency);
}
}
} }
for (;;) { for (;;) {
@@ -32,5 +32,9 @@ private:
struct SoundIoOutStream* m_OutputStream; struct SoundIoOutStream* m_OutputStream;
struct SoundIoRingBuffer* m_RingBuffer; struct SoundIoRingBuffer* m_RingBuffer;
struct SoundIoChannelLayout m_EffectiveLayout; struct SoundIoChannelLayout m_EffectiveLayout;
double m_Latency;
bool m_Errored; bool m_Errored;
static const double k_RawSampleLengthSec;
static const double k_MinSampleLengthSec;
}; };
+224 -77
View File
@@ -17,6 +17,8 @@
#define VK_NUMPAD0 0x60 #define VK_NUMPAD0 0x60
#endif #endif
#define MOUSE_POLLING_INTERVAL 5
// How long the mouse button will be pressed for a tap to click gesture // How long the mouse button will be pressed for a tap to click gesture
#define TAP_BUTTON_RELEASE_DELAY 100 #define TAP_BUTTON_RELEASE_DELAY 100
@@ -34,10 +36,9 @@ const int SdlInputHandler::k_ButtonMap[] = {
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
}; };
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer, int streamWidth, int streamHeight) SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
: m_LastMouseMotionTime(0), : m_MultiController(prefs.multiController),
m_MultiController(prefs.multiController), m_MouseMoveTimer(0),
m_NeedsInputDelay(false),
m_LeftButtonReleaseTimer(0), m_LeftButtonReleaseTimer(0),
m_RightButtonReleaseTimer(0), m_RightButtonReleaseTimer(0),
m_DragTimer(0), m_DragTimer(0),
@@ -56,6 +57,21 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* comput
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.mouseAcceleration ? "1" : "0"); prefs.mouseAcceleration ? "1" : "0");
// We must initialize joystick explicitly before gamecontroller in order
// to ensure we receive gamecontroller attach events for gamepads where
// SDL doesn't have a built-in mapping. By starting joystick first, we
// can allow mapping manager to update the mappings before GC attach
// events are generated.
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
// We need to reinit this each time, since you only get // We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init. // an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER)); SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
@@ -65,50 +81,52 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* comput
SDL_GetError()); SDL_GetError());
} }
MappingManager mappingManager; SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
mappingManager.applyMappings(); if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
if (!m_MultiController) { "SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
// Player 1 is always present in non-MC mode SDL_GetError());
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 // Initialize the gamepad mask with currently attached gamepads to avoid
// GFE to choke and input latency to increase significantly. We will // causing gamepads to unexpectedly disappear and reappear on the host
// artificially throttle them to avoid this situation. // during stream startup as we detect currently attached gamepads one at a time.
QVector<int> gfeVersion = NvHTTP::parseQuad(computer->gfeVersion); m_GamepadMask = getAttachedGamepadMask();
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);
SDL_zero(m_TouchDownEvent); SDL_zero(m_TouchDownEvent);
SDL_zero(m_CumulativeDelta); SDL_zero(m_CumulativeDelta);
SDL_AtomicSet(&m_MouseDeltaX, 0);
SDL_AtomicSet(&m_MouseDeltaY, 0);
m_MouseMoveTimer = SDL_AddTimer(MOUSE_POLLING_INTERVAL, SdlInputHandler::mouseMoveTimerCallback, this);
} }
SdlInputHandler::~SdlInputHandler() SdlInputHandler::~SdlInputHandler()
{ {
for (int i = 0; i < MAX_GAMEPADS; i++) { for (int i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].haptic != nullptr) {
SDL_HapticClose(m_GamepadState[i].haptic);
}
if (m_GamepadState[i].controller != nullptr) { if (m_GamepadState[i].controller != nullptr) {
SDL_GameControllerClose(m_GamepadState[i].controller); SDL_GameControllerClose(m_GamepadState[i].controller);
} }
} }
SDL_RemoveTimer(m_MouseMoveTimer);
SDL_RemoveTimer(m_LeftButtonReleaseTimer); SDL_RemoveTimer(m_LeftButtonReleaseTimer);
SDL_RemoveTimer(m_RightButtonReleaseTimer); SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer); SDL_RemoveTimer(m_DragTimer);
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER)); SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
} }
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
@@ -121,10 +139,21 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
(event->keysym.mod & KMOD_CTRL) && (event->keysym.mod & KMOD_CTRL) &&
(event->keysym.mod & KMOD_ALT) && (event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) { (event->keysym.mod & KMOD_SHIFT)) {
// First we test the SDLK combos for matches,
// that way we ensure that latin keyboard users
// can match to the key they see on their keyboards.
// If nothing matches that, we'll then go on to
// checking scancodes so non-latin keyboard users
// can have working hotkeys (though possibly in
// odd positions). We must do all SDLK tests before
// any scancode tests to avoid issues in cases
// where the SDLK for one shortcut collides with
// the scancode of another.
// 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 (SDLK)");
// Push a quit event to the main loop // Push a quit event to the main loop
SDL_Event event; SDL_Event event;
@@ -136,7 +165,55 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
// Check for the unbind combo (Ctrl+Alt+Shift+Z) // Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.sym == SDLK_z) { else if (event->keysym.sym == SDLK_z) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo"); "Detected mouse capture toggle combo (SDLK)");
// Stop handling future input
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
raiseAllKeys();
return;
}
else if (event->keysym.sym == SDLK_x) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo (SDLK)");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
else if (event->keysym.sym == SDLK_s) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo (SDLK)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
// Check for quit combo (Ctrl+Alt+Shift+Q)
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo (scancode)");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
return;
}
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.scancode == SDL_SCANCODE_Z) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo (scancode)");
// Stop handling future input // Stop handling future input
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode()); SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
@@ -147,11 +224,24 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
return; return;
} }
// Check for the full-screen combo (Ctrl+Alt+Shift+X) // Check for the full-screen combo (Ctrl+Alt+Shift+X)
else if (event->keysym.sym == SDLK_x) { else if (event->keysym.scancode == SDL_SCANCODE_X) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo"); "Detected full-screen toggle combo (scancode)");
Session::s_ActiveSession->toggleFullscreen(); Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
else if (event->keysym.scancode == SDL_SCANCODE_S) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo (scancode)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Force raise all keys just be safe across this full-screen/windowed // Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost. // transition just in case key events get lost.
raiseAllKeys(); raiseAllKeys();
@@ -459,40 +549,10 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
return; return;
} }
short xdelta = (short)event->xrel; // Batch until the next mouse polling window or we'll get awful
short ydelta = (short)event->yrel; // input lag everything except GFE 3.14 and 3.15.
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
// If we're sending more than one motion event per millisecond, SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
// delay for 1 ms to allow batching of mouse move events. On older
// 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);
}
} }
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
@@ -572,6 +632,20 @@ Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
return 0; return 0;
} }
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
return interval;
}
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event) void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
{ {
GamepadState* state = findStateForGamepad(event->which); GamepadState* state = findStateForGamepad(event->which);
@@ -655,8 +729,26 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
if (event->type == SDL_CONTROLLERDEVICEADDED) { if (event->type == SDL_CONTROLLERDEVICEADDED) {
int i; int i;
const char* name; const char* name;
SDL_GameController* controller;
const char* mapping;
char guidStr[33];
for (i = 0; i < MAX_GAMEPADS; i++) { controller = SDL_GameControllerOpen(event->which);
if (controller == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open gamepad: %s",
SDL_GetError());
return;
}
// We used to use SDL_GameControllerGetPlayerIndex() here but that
// can lead to strange issues due to bugs in Windows where an Xbox
// controller will join as player 2, even though no player 1 controller
// is connected at all. This pretty much screws any attempt to use
// the gamepad in single player games, so just assign them in order from 0.
i = 0;
for (; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].controller == NULL) { if (m_GamepadState[i].controller == NULL) {
// Found an empty slot // Found an empty slot
break; break;
@@ -666,6 +758,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
if (i == MAX_GAMEPADS) { if (i == MAX_GAMEPADS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No open gamepad slots found!"); "No open gamepad slots found!");
SDL_GameControllerClose(controller);
return; return;
} }
@@ -677,26 +770,36 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
// Always player 1 in single controller mode // Always player 1 in single controller mode
state->index = 0; state->index = 0;
} }
state->controller = SDL_GameControllerOpen(event->which);
if (state->controller == NULL) { state->controller = controller;
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
"Failed to open gamepad: %s", state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
SDL_GetError()); state->hapticEffectId = -1;
return; if (state->haptic != nullptr && (SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
SDL_HapticClose(state->haptic);
state->haptic = nullptr;
} }
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller)); SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
guidStr, sizeof(guidStr));
mapping = SDL_GameControllerMapping(state->controller);
name = SDL_GameControllerName(state->controller); name = SDL_GameControllerName(state->controller);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d (player %d) is: %s", "Gamepad %d (player %d) is: %s (%s -> %s)",
i, i,
state->index, state->index,
name != NULL ? name : "<null>"); name != nullptr ? name : "<null>",
guidStr,
mapping != nullptr ? mapping : "<null>");
if (mapping != nullptr) {
SDL_free((void*)mapping);
}
// Add this gamepad to the gamepad mask // Add this gamepad to the gamepad mask
if (m_MultiController) { if (m_MultiController) {
SDL_assert(!(m_GamepadMask & (1 << state->index))); // NB: Don't assert that it's unset here because we will already
// have the mask set for initially attached gamepads to avoid confusing
// apps running on the host.
m_GamepadMask |= (1 << state->index); m_GamepadMask |= (1 << state->index);
} }
else { else {
@@ -710,6 +813,9 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
state = findStateForGamepad(event->which); state = findStateForGamepad(event->which);
if (state != NULL) { if (state != NULL) {
SDL_GameControllerClose(state->controller); SDL_GameControllerClose(state->controller);
if (state->haptic != nullptr) {
SDL_HapticClose(state->haptic);
}
// Remove this from the gamepad mask in MC-mode // Remove this from the gamepad mask in MC-mode
if (m_MultiController) { if (m_MultiController) {
@@ -763,6 +869,47 @@ void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
} }
} }
void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
{
// Make sure the controller number is within our supported count
if (controllerNumber >= MAX_GAMEPADS) {
return;
}
// Check if the controller supports haptics (and if the controller exists at all)
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
if (haptic == nullptr) {
return;
}
// Stop the last effect we played
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
}
// If this callback is telling us to stop both motors, don't bother queuing a new effect
if (lowFreqMotor == 0 && highFreqMotor == 0) {
return;
}
SDL_HapticEffect effect;
SDL_memset(&effect, 0, sizeof(effect));
effect.type = SDL_HAPTIC_LEFTRIGHT;
// The effect should last until we are instructed to stop or change it
effect.leftright.length = SDL_HAPTIC_INFINITY;
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
effect.leftright.large_magnitude = lowFreqMotor / 2;
effect.leftright.small_magnitude = highFreqMotor / 2;
// Play the new effect
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
}
}
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event) void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
{ {
int fingerIndex = -1; int fingerIndex = -1;
@@ -968,7 +1115,7 @@ QString SdlInputHandler::getUnmappedGamepads()
"Number of axes: %d | Number of buttons: %d | Number of hats: %d", "Number of axes: %d | Number of buttons: %d | Number of hats: %d",
numAxes, numButtons, numHats); numAxes, numButtons, numHats);
if (numAxes >= 4 && numButtons >= 8 && numHats <= 1) { if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick likely to be an unmapped game controller"); "Joystick likely to be an unmapped game controller");
if (!ret.isEmpty()) { if (!ret.isEmpty()) {
+10 -2
View File
@@ -8,6 +8,8 @@
struct GamepadState { struct GamepadState {
SDL_GameController* controller; SDL_GameController* controller;
SDL_JoystickID jsId; SDL_JoystickID jsId;
SDL_Haptic* haptic;
int hapticEffectId;
short index; short index;
short buttons; short buttons;
@@ -43,6 +45,8 @@ public:
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
void handleTouchFingerEvent(SDL_TouchFingerEvent* event); void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
int getAttachedGamepadMask(); int getAttachedGamepadMask();
@@ -67,9 +71,13 @@ private:
static static
Uint32 dragTimerCallback(Uint32 interval, void* param); Uint32 dragTimerCallback(Uint32 interval, void* param);
Uint32 m_LastMouseMotionTime; static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
bool m_MultiController; bool m_MultiController;
bool m_NeedsInputDelay; SDL_TimerID m_MouseMoveTimer;
SDL_atomic_t m_MouseDeltaX;
SDL_atomic_t m_MouseDeltaY;
int m_GamepadMask; int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS]; GamepadState m_GamepadState[MAX_GAMEPADS];
QSet<short> m_KeysDown; QSet<short> m_KeysDown;
+158 -78
View File
@@ -6,12 +6,6 @@
#include <SDL.h> #include <SDL.h>
#include "utils.h" #include "utils.h"
// HACK: Need to call SetThreadExecutionState() because SDL doesn't
#ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#endif
#ifdef HAVE_FFMPEG #ifdef HAVE_FFMPEG
#include "video/ffmpeg.h" #include "video/ffmpeg.h"
#endif #endif
@@ -44,7 +38,8 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clConnectionTerminated, Session::clConnectionTerminated,
nullptr, nullptr,
nullptr, nullptr,
Session::clLogMessage Session::clLogMessage,
Session::clRumble
}; };
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = { AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
@@ -79,7 +74,11 @@ void Session::clStageFailed(int stage, long errorCode)
void Session::clConnectionTerminated(long errorCode) void Session::clConnectionTerminated(long errorCode)
{ {
// Display the termination dialog if this was not intended
if (errorCode != 0) {
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError("Connection terminated"); emit s_ActiveSession->displayLaunchError("Connection terminated");
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Connection terminated: %ld", "Connection terminated: %ld",
@@ -104,14 +103,29 @@ void Session::clLogMessage(const char* format, ...)
va_end(ap); va_end(ap);
} }
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync) void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
{
// The input handler can be closed during the stream if LiStopConnection() hasn't completed yet
// but the stream has been stopped by the user. In this case, just discard the rumble.
SDL_AtomicLock(&s_ActiveSession->m_InputHandlerLock);
if (s_ActiveSession->m_InputHandler != nullptr) {
s_ActiveSession->m_InputHandler->rumble(controllerNumber, lowFreqMotor, highFreqMotor);
}
SDL_AtomicUnlock(&s_ActiveSession->m_InputHandlerLock);
}
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height, SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder) int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder)
{ {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync %s",
enableVsync ? "enabled" : "disabled");
#ifdef HAVE_SLVIDEO #ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder(); chosenDecoder = new SLVideoDecoder(testOnly);
if (CALL_INITIALIZE(chosenDecoder)) { if (CALL_INITIALIZE(chosenDecoder)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo video decoder chosen"); "SLVideo video decoder chosen");
@@ -126,7 +140,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
#endif #endif
#ifdef HAVE_FFMPEG #ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder(); chosenDecoder = new FFmpegVideoDecoder(testOnly);
if (CALL_INITIALIZE(chosenDecoder)) { if (CALL_INITIALIZE(chosenDecoder)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"FFmpeg-based video decoder chosen"); "FFmpeg-based video decoder chosen");
@@ -222,17 +236,20 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
return false; return false;
} }
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) { if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false; return false;
} }
SDL_DestroyWindow(window);
bool ret = decoder->isHardwareAccelerated(); bool ret = decoder->isHardwareAccelerated();
delete decoder; delete decoder;
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return ret; return ret;
@@ -259,17 +276,20 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection
return false; return false;
} }
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) { if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false; return false;
} }
SDL_DestroyWindow(window);
int caps = decoder->getDecoderCapabilities(); int caps = decoder->getDecoderCapabilities();
delete decoder; delete decoder;
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return caps; return caps;
@@ -287,12 +307,26 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
m_DisplayOriginX(0), m_DisplayOriginX(0),
m_DisplayOriginY(0), m_DisplayOriginY(0),
m_PendingWindowedTransition(false), m_PendingWindowedTransition(false),
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
m_InputHandler(nullptr),
m_InputHandlerLock(0),
m_OpusDecoder(nullptr), m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr), m_AudioRenderer(nullptr),
m_AudioSampleCount(0) m_AudioSampleCount(0),
m_DropAudioEndTime(0)
{ {
} }
// NB: This may not get destroyed for a long time! Don't put any vital cleanup here.
// Use Session::exec() or DeferredSessionCleanupTask instead.
Session::~Session()
{
// Acquire session semaphore to ensure all cleanup is done before the destructor returns
// and the object is deallocated.
s_ActiveSessionSemaphore.acquire();
s_ActiveSessionSemaphore.release();
}
void Session::initialize() void Session::initialize()
{ {
qInfo() << "Server GPU:" << m_Computer->gpuModel; qInfo() << "Server GPU:" << m_Computer->gpuModel;
@@ -315,6 +349,8 @@ void Session::initialize()
m_StreamConfig.fps = m_Preferences->fps; m_StreamConfig.fps = m_Preferences->fps;
m_StreamConfig.bitrate = m_Preferences->bitrateKbps; m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75; m_StreamConfig.hevcBitratePercentageMultiplier = 75;
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps", "Video bitrate: %d kbps",
@@ -382,20 +418,6 @@ void Session::initialize()
break; break;
} }
if (m_Computer->activeAddress == m_Computer->remoteAddress) {
m_StreamConfig.streamingRemotely = 1;
}
else {
m_StreamConfig.streamingRemotely = 0;
}
if (m_Computer->activeAddress == m_Computer->localAddress) {
m_StreamConfig.packetSize = 1392;
}
else {
m_StreamConfig.packetSize = 1024;
}
switch (m_Preferences->windowMode) switch (m_Preferences->windowMode)
{ {
case StreamingPreferences::WM_FULLSCREEN_DESKTOP: case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
@@ -505,15 +527,6 @@ bool Session::validateLaunch()
if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) { if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) {
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming."); emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
m_StreamConfig.width = 1920;
m_StreamConfig.height = 1080;
}
// This list is sorted from least to greatest
else if (m_Computer->displayModes.last().width < 3840 ||
(m_Computer->displayModes.last().refreshRate < 60 && m_StreamConfig.fps >= 60)) {
emitLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
m_StreamConfig.width = 1920; m_StreamConfig.width = 1920;
m_StreamConfig.height = 1080; m_StreamConfig.height = 1080;
} }
@@ -572,25 +585,65 @@ bool Session::validateLaunch()
class DeferredSessionCleanupTask : public QRunnable class DeferredSessionCleanupTask : public QRunnable
{ {
void run() override public:
{ DeferredSessionCleanupTask(Session* session) :
// Finish cleanup of the connection state m_Session(session) {}
LiStopConnection();
private:
virtual ~DeferredSessionCleanupTask() override
{
// Allow another session to start now that we're cleaned up // Allow another session to start now that we're cleaned up
Session::s_ActiveSession = nullptr; Session::s_ActiveSession = nullptr;
Session::s_ActiveSessionSemaphore.release(); Session::s_ActiveSessionSemaphore.release();
} }
void run() override
{
// Only quit the running app if our session terminated gracefully
bool shouldQuit =
!m_Session->m_UnexpectedTermination &&
m_Session->m_Preferences->quitAppAfter;
// Notify the UI
if (shouldQuit) {
emit m_Session->quitStarting();
}
else {
emit m_Session->sessionFinished();
}
// Finish cleanup of the connection state
LiStopConnection();
// Perform a best-effort app quit
if (shouldQuit) {
NvHTTP http(m_Session->m_Computer->activeAddress, m_Session->m_Computer->serverCert);
// Logging is already done inside NvHTTP
try {
http.quitApp();
} catch (const GfeHttpResponseException&) {
} catch (const QtNetworkReplyException&) {
}
// Session is finished now
emit m_Session->sessionFinished();
}
}
Session* m_Session;
}; };
void Session::getWindowDimensions(int& x, int& y, void Session::getWindowDimensions(int& x, int& y,
int& width, int& height) int& width, int& height)
{ {
int displayIndex = 0; int displayIndex = 0;
bool fullScreen;
if (m_Window != nullptr) { if (m_Window != nullptr) {
displayIndex = SDL_GetWindowDisplayIndex(m_Window); displayIndex = SDL_GetWindowDisplayIndex(m_Window);
SDL_assert(displayIndex >= 0); SDL_assert(displayIndex >= 0);
fullScreen = (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN);
} }
// Create our window on the same display that Qt's UI // Create our window on the same display that Qt's UI
// was being displayed on. // was being displayed on.
@@ -617,10 +670,16 @@ void Session::getWindowDimensions(int& x, int& y,
i, SDL_GetError()); i, SDL_GetError());
} }
} }
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED);
} }
SDL_Rect usableBounds; SDL_Rect usableBounds;
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) { if (fullScreen && SDL_GetDisplayBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w;
height = usableBounds.h;
}
else if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w; width = usableBounds.w;
height = usableBounds.h; height = usableBounds.h;
@@ -746,6 +805,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Check for validation errors/warnings and emit // Check for validation errors/warnings and emit
// signals for them, if appropriate // signals for them, if appropriate
if (!validateLaunch()) { if (!validateLaunch()) {
emit sessionFinished();
return; return;
} }
@@ -766,7 +826,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Initialize the gamepad code with our preferences // Initialize the gamepad code with our preferences
StreamingPreferences prefs; StreamingPreferences prefs;
SdlInputHandler inputHandler(prefs, m_Computer, m_InputHandler = new SdlInputHandler(prefs, m_Computer,
m_StreamConfig.width, m_StreamConfig.width,
m_StreamConfig.height); m_StreamConfig.height);
@@ -775,6 +835,9 @@ void Session::exec(int displayOriginX, int displayOriginY)
Q_ASSERT(m_Computer->currentGameId == 0 || Q_ASSERT(m_Computer->currentGameId == 0 ||
m_Computer->currentGameId == m_App.id); m_Computer->currentGameId == m_App.id);
// SOPS will set all settings to 720p60 if it doesn't recognize
// the chosen resolution. Avoid that by disabling SOPS when it
// is not streaming a supported resolution.
bool enableGameOptimizations = false; bool enableGameOptimizations = false;
for (const NvDisplayMode &mode : m_Computer->displayModes) { for (const NvDisplayMode &mode : m_Computer->displayModes) {
if (mode.width == m_StreamConfig.width && if (mode.width == m_StreamConfig.width &&
@@ -788,7 +851,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
} }
try { try {
NvHTTP http(m_Computer->activeAddress); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
if (m_Computer->currentGameId != 0) { if (m_Computer->currentGameId != 0) {
http.resumeApp(&m_StreamConfig); http.resumeApp(&m_StreamConfig);
} }
@@ -796,15 +859,19 @@ void Session::exec(int displayOriginX, int displayOriginY)
http.launchApp(m_App.id, &m_StreamConfig, http.launchApp(m_App.id, &m_StreamConfig,
enableGameOptimizations, enableGameOptimizations,
prefs.playAudioOnHost, prefs.playAudioOnHost,
inputHandler.getAttachedGamepadMask()); m_InputHandler->getAttachedGamepadMask());
} }
} catch (const GfeHttpResponseException& e) { } catch (const GfeHttpResponseException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
emit displayLaunchError(e.toQString()); emit displayLaunchError(e.toQString());
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} catch (const QtNetworkReplyException& e) { } catch (const QtNetworkReplyException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
emit displayLaunchError(e.toQString()); emit displayLaunchError(e.toQString());
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -813,8 +880,10 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s", "SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError()); SDL_GetError());
delete m_InputHandler;
m_InputHandler = nullptr;
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError())); emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -841,8 +910,10 @@ void Session::exec(int displayOriginX, int displayOriginY)
if (err != 0) { if (err != 0) {
// We already displayed an error dialog in the stage failure // We already displayed an error dialog in the stage failure
// listener. // listener.
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -863,8 +934,10 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s", "SDL_CreateWindow() failed: %s",
SDL_GetError()); SDL_GetError());
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -930,13 +1003,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Disable the screen saver // Disable the screen saver
SDL_DisableScreenSaver(); SDL_DisableScreenSaver();
#ifdef Q_OS_WIN32
// HACK: SDL doesn't call this, so we must do so to disable the
// screensaver when we're in windowed mode. DirectX will disable
// it for us when we're in full-screen exclusive mode.
SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
#endif
// Set timer resolution to 1 ms on Windows for greater // Set timer resolution to 1 ms on Windows for greater
// sleep precision and more accurate callback timing. // sleep precision and more accurate callback timing.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1"); SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
@@ -951,6 +1017,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window); int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
// Now that we're about to stream, any SDL_QUIT event is expected
// unless it comes from the connection termination callback where
// (m_UnexpectedTermination is set back to true).
m_UnexpectedTermination = false;
// Hijack this thread to be the SDL main thread. We have to do this // Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over. // because we want to suspend all Qt processing until the stream is over.
SDL_Event event; SDL_Event event;
@@ -1026,7 +1097,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Raise all keys that are currently pressed. If we don't do this, certain keys // Raise all keys that are currently pressed. If we don't do this, certain keys
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down. // used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
inputHandler.raiseAllKeys(); m_InputHandler->raiseAllKeys();
} }
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event. // We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
@@ -1098,6 +1169,8 @@ void Session::exec(int displayOriginX, int displayOriginY)
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth, m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate, m_ActiveVideoHeight, m_ActiveVideoFrameRate,
enableVsync, enableVsync,
enableVsync && m_Preferences->framePacing,
false,
s_ActiveSession->m_VideoDecoder)) { s_ActiveSession->m_VideoDecoder)) {
SDL_AtomicUnlock(&m_DecoderLock); SDL_AtomicUnlock(&m_DecoderLock);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -1115,31 +1188,31 @@ void Session::exec(int displayOriginX, int displayOriginY)
case SDL_KEYUP: case SDL_KEYUP:
case SDL_KEYDOWN: case SDL_KEYDOWN:
inputHandler.handleKeyEvent(&event.key); m_InputHandler->handleKeyEvent(&event.key);
break; break;
case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP: case SDL_MOUSEBUTTONUP:
inputHandler.handleMouseButtonEvent(&event.button); m_InputHandler->handleMouseButtonEvent(&event.button);
break; break;
case SDL_MOUSEMOTION: case SDL_MOUSEMOTION:
inputHandler.handleMouseMotionEvent(&event.motion); m_InputHandler->handleMouseMotionEvent(&event.motion);
break; break;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
inputHandler.handleMouseWheelEvent(&event.wheel); m_InputHandler->handleMouseWheelEvent(&event.wheel);
break; break;
case SDL_CONTROLLERAXISMOTION: case SDL_CONTROLLERAXISMOTION:
inputHandler.handleControllerAxisEvent(&event.caxis); m_InputHandler->handleControllerAxisEvent(&event.caxis);
break; break;
case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP: case SDL_CONTROLLERBUTTONUP:
inputHandler.handleControllerButtonEvent(&event.cbutton); m_InputHandler->handleControllerButtonEvent(&event.cbutton);
break; break;
case SDL_CONTROLLERDEVICEADDED: case SDL_CONTROLLERDEVICEADDED:
case SDL_CONTROLLERDEVICEREMOVED: case SDL_CONTROLLERDEVICEREMOVED:
inputHandler.handleControllerDeviceEvent(&event.cdevice); m_InputHandler->handleControllerDeviceEvent(&event.cdevice);
break; break;
case SDL_JOYDEVICEADDED: case SDL_JOYDEVICEADDED:
inputHandler.handleJoystickArrivalEvent(&event.jdevice); m_InputHandler->handleJoystickArrivalEvent(&event.jdevice);
break; break;
// SDL2 sends touch events from trackpads by default on // SDL2 sends touch events from trackpads by default on
@@ -1149,18 +1222,13 @@ void Session::exec(int displayOriginX, int displayOriginY)
case SDL_FINGERDOWN: case SDL_FINGERDOWN:
case SDL_FINGERMOTION: case SDL_FINGERMOTION:
case SDL_FINGERUP: case SDL_FINGERUP:
inputHandler.handleTouchFingerEvent(&event.tfinger); m_InputHandler->handleTouchFingerEvent(&event.tfinger);
break; break;
#endif #endif
} }
} }
DispatchDeferredCleanup: DispatchDeferredCleanup:
#ifdef Q_OS_WIN32
// HACK: See comment above
SetThreadExecutionState(ES_CONTINUOUS);
#endif
// Uncapture the mouse and hide the window immediately, // Uncapture the mouse and hide the window immediately,
// so we can return to the Qt GUI ASAP. // so we can return to the Qt GUI ASAP.
SDL_SetRelativeMouseMode(SDL_FALSE); SDL_SetRelativeMouseMode(SDL_FALSE);
@@ -1168,7 +1236,16 @@ DispatchDeferredCleanup:
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0"); SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
// Raise any keys that are still down // Raise any keys that are still down
inputHandler.raiseAllKeys(); m_InputHandler->raiseAllKeys();
// Destroy the input handler now. Any rumble callbacks that
// occur after this point will be discarded. This must be destroyed
// before allow the UI to continue execution or it could interfere
// with SDLGamepadKeyNavigation.
SDL_AtomicLock(&m_InputHandlerLock);
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_AtomicUnlock(&m_InputHandlerLock);
// Destroy the decoder, since this must be done on the main thread // Destroy the decoder, since this must be done on the main thread
SDL_AtomicLock(&m_DecoderLock); SDL_AtomicLock(&m_DecoderLock);
@@ -1176,7 +1253,10 @@ DispatchDeferredCleanup:
m_VideoDecoder = nullptr; m_VideoDecoder = nullptr;
SDL_AtomicUnlock(&m_DecoderLock); SDL_AtomicUnlock(&m_DecoderLock);
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(m_Window); SDL_DestroyWindow(m_Window);
if (iconSurface != nullptr) { if (iconSurface != nullptr) {
SDL_FreeSurface(iconSurface); SDL_FreeSurface(iconSurface);
} }
@@ -1186,6 +1266,6 @@ DispatchDeferredCleanup:
// Cleanup can take a while, so dispatch it to a worker thread. // Cleanup can take a while, so dispatch it to a worker thread.
// When it is complete, it will release our s_ActiveSessionSemaphore // When it is complete, it will release our s_ActiveSessionSemaphore
// reference. // reference.
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask()); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
} }
+29 -2
View File
@@ -4,11 +4,11 @@
#include <Limelight.h> #include <Limelight.h>
#include <opus_multistream.h> #include <opus_multistream.h>
#include "backend/computermanager.h"
#include "settings/streamingpreferences.h" #include "settings/streamingpreferences.h"
#include "input.h" #include "input.h"
#include "video/decoder.h" #include "video/decoder.h"
#include "audio/renderers/renderer.h" #include "audio/renderers/renderer.h"
#include "video/overlaymanager.h"
class Session : public QObject class Session : public QObject
{ {
@@ -20,6 +20,8 @@ class Session : public QObject
public: public:
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr); explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
virtual ~Session();
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY); Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static static
@@ -30,6 +32,16 @@ public:
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds, int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate); int videoFormat, int width, int height, int frameRate);
static Session* get()
{
return s_ActiveSession;
}
Overlay::OverlayManager& getOverlayManager()
{
return m_OverlayManager;
}
signals: signals:
void stageStarting(QString stage); void stageStarting(QString stage);
@@ -41,6 +53,10 @@ signals:
void displayLaunchWarning(QString text); void displayLaunchWarning(QString text);
void quitStarting();
void sessionFinished();
private: private:
void initialize(); void initialize();
@@ -66,7 +82,9 @@ private:
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, bool enableVsync, IVideoDecoder*& chosenDecoder); int frameRate, bool enableVsync, bool enableFramePacing,
bool testOnly,
IVideoDecoder*& chosenDecoder);
static static
void clStageStarting(int stage); void clStageStarting(int stage);
@@ -80,6 +98,9 @@ private:
static static
void clLogMessage(const char* format, ...); void clLogMessage(const char* format, ...);
static
void clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
static static
int arInit(int audioConfiguration, int arInit(int audioConfiguration,
const POPUS_MULTISTREAM_CONFIGURATION opusConfig, const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
@@ -114,6 +135,9 @@ private:
int m_DisplayOriginX; int m_DisplayOriginX;
int m_DisplayOriginY; int m_DisplayOriginY;
bool m_PendingWindowedTransition; bool m_PendingWindowedTransition;
bool m_UnexpectedTermination;
SdlInputHandler* m_InputHandler;
SDL_SpinLock m_InputHandlerLock;
int m_ActiveVideoFormat; int m_ActiveVideoFormat;
int m_ActiveVideoWidth; int m_ActiveVideoWidth;
@@ -125,6 +149,9 @@ private:
IAudioRenderer* m_AudioRenderer; IAudioRenderer* m_AudioRenderer;
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig; OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
int m_AudioSampleCount; int m_AudioSampleCount;
Uint32 m_DropAudioEndTime;
Overlay::OverlayManager m_OverlayManager;
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks; static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks; static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
+5 -1
View File
@@ -12,11 +12,14 @@ typedef struct _VIDEO_STATS {
uint32_t receivedFrames; uint32_t receivedFrames;
uint32_t decodedFrames; uint32_t decodedFrames;
uint32_t renderedFrames; uint32_t renderedFrames;
uint32_t totalFrames;
uint32_t networkDroppedFrames; uint32_t networkDroppedFrames;
uint32_t pacerDroppedFrames; uint32_t pacerDroppedFrames;
uint32_t totalReassemblyTime; uint32_t totalReassemblyTime;
uint32_t totalDecodeTime; uint32_t totalDecodeTime;
uint32_t totalPacerTime;
uint32_t totalRenderTime; uint32_t totalRenderTime;
float totalFps;
float receivedFps; float receivedFps;
float decodedFps; float decodedFps;
float renderedFps; float renderedFps;
@@ -32,7 +35,8 @@ public:
int width, int width,
int height, int height,
int frameRate, int frameRate,
bool enableVsync) = 0; bool enableVsync,
bool enableFramePacing) = 0;
virtual bool isHardwareAccelerated() = 0; virtual bool isHardwareAccelerated() = 0;
virtual int getDecoderCapabilities() = 0; virtual int getDecoderCapabilities() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0; virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
+160 -11
View File
@@ -2,6 +2,7 @@
#include "dxva2.h" #include "dxva2.h"
#include "../ffmpeg.h" #include "../ffmpeg.h"
#include <streaming/streamutils.h> #include <streaming/streamutils.h>
#include <streaming/session.h>
#include <SDL_syswm.h> #include <SDL_syswm.h>
@@ -25,7 +26,9 @@ DXVA2Renderer::DXVA2Renderer() :
m_RenderTarget(nullptr), m_RenderTarget(nullptr),
m_ProcService(nullptr), m_ProcService(nullptr),
m_Processor(nullptr), m_Processor(nullptr),
m_FrameIndex(0) m_FrameIndex(0),
m_DebugOverlayFont(nullptr),
m_BlockingPresent(false)
{ {
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces)); RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext)); RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
@@ -44,6 +47,7 @@ DXVA2Renderer::~DXVA2Renderer()
SAFE_COM_RELEASE(m_RenderTarget); SAFE_COM_RELEASE(m_RenderTarget);
SAFE_COM_RELEASE(m_ProcService); SAFE_COM_RELEASE(m_ProcService);
SAFE_COM_RELEASE(m_Processor); SAFE_COM_RELEASE(m_Processor);
SAFE_COM_RELEASE(m_DebugOverlayFont);
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) { for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
SAFE_COM_RELEASE(m_DecSurfaces[i]); SAFE_COM_RELEASE(m_DecSurfaces[i]);
@@ -256,6 +260,7 @@ bool DXVA2Renderer::initializeRenderer()
m_DisplayWidth = renderTargetDesc.Width; m_DisplayWidth = renderTargetDesc.Width;
m_DisplayHeight = renderTargetDesc.Height; m_DisplayHeight = renderTargetDesc.Height;
if (!isDXVideoProcessorAPIBlacklisted()) {
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService, hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
reinterpret_cast<void**>(&m_ProcService)); reinterpret_cast<void**>(&m_ProcService));
@@ -314,10 +319,55 @@ bool DXVA2Renderer::initializeRenderer()
hr); hr);
return false; return false;
} }
}
return true; return true;
} }
bool DXVA2Renderer::isDXVideoProcessorAPIBlacklisted()
{
IDirect3D9* d3d9;
HRESULT hr;
bool result = false;
hr = m_Device->GetDirect3D(&d3d9);
if (SUCCEEDED(hr)) {
D3DCAPS9 caps;
hr = m_Device->GetDeviceCaps(&caps);
if (SUCCEEDED(hr)) {
D3DADAPTER_IDENTIFIER9 id;
hr = d3d9->GetAdapterIdentifier(caps.AdapterOrdinal, 0, &id);
if (SUCCEEDED(hr) && id.VendorId == 0x8086) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Avoiding IDirectXVideoProcessor API on Intel GPU");
// On Intel GPUs, we can get unwanted video "enhancements" due to post-processing
// effects that the GPU driver forces on us. In many cases, this makes the video
// actually look worse. We can avoid these by using StretchRect() instead on these
// platforms.
result = true;
}
else {
result = false;
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetDeviceCaps() failed: %x", hr);
}
d3d9->Release();
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetDirect3D() failed: %x", hr);
}
return result;
}
bool DXVA2Renderer::isDecoderBlacklisted() bool DXVA2Renderer::isDecoderBlacklisted()
{ {
IDirect3D9* d3d9; IDirect3D9* d3d9;
@@ -512,6 +562,8 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.BackBufferCount = 1; d3dpp.BackBufferCount = 1;
} }
m_BlockingPresent = false;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Windowed mode with DWM running"); "Windowed mode with DWM running");
} }
@@ -521,6 +573,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1; d3dpp.BackBufferCount = 1;
m_BlockingPresent = true;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync enabled"); "V-Sync enabled");
@@ -531,6 +584,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1; d3dpp.BackBufferCount = 1;
m_BlockingPresent = false;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync disabled in tearing mode"); "V-Sync disabled in tearing mode");
@@ -591,9 +645,13 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
int alignment; int alignment;
// HEVC using DXVA requires 128B alignment // HEVC using DXVA requires 128 pixel alignment, however Intel GPUs decoding HEVC
// using StretchRect() to render draw a translucent green line at the top of
// the screen in full-screen mode at 720p/1080p unless we use 32 pixel alignment.
// This appears to work without issues on AMD and Nvidia GPUs too, so we will
// do it unconditionally for now.
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) { if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
alignment = 128; alignment = 32;
} }
else { else {
alignment = 16; alignment = 16;
@@ -644,12 +702,39 @@ int DXVA2Renderer::getDecoderCapabilities()
return 0; return 0;
} }
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
} }
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame) void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType)
{
HRESULT hr;
// Initialize the overlay font if it's not already created
if (m_DebugOverlayFont == nullptr) {
hr = D3DXCreateFontA(m_Device,
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug),
0,
FW_HEAVY,
1,
false,
ANSI_CHARSET,
OUT_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE,
"",
&m_DebugOverlayFont);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DXCreateFontA() failed: %x",
hr);
m_DebugOverlayFont = nullptr;
}
}
}
void DXVA2Renderer::renderFrame(AVFrame *frame)
{ {
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]); IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
HRESULT hr; HRESULT hr;
@@ -785,12 +870,10 @@ void DXVA2Renderer::renderFrameAtVsync(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_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);
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VideoProcessBlt() failed: %x", "Clear() failed: %x",
hr); hr);
SDL_Event event; SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET; event.type = SDL_RENDER_TARGETS_RESET;
@@ -798,7 +881,73 @@ void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
return; return;
} }
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, 0); hr = m_Device->BeginScene();
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"BeginScene() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
if (m_Processor) {
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VideoProcessBlt() failed, falling back to StretchRect(): %x",
hr);
m_Processor->Release();
m_Processor = nullptr;
}
}
if (!m_Processor) {
// This function doesn't trigger any of Intel's garbage video "enhancements"
hr = m_Device->StretchRect(surface, &sample.SrcRect, m_RenderTarget, &sample.DstRect, D3DTEXF_NONE);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"StretchRect() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
}
if (m_DebugOverlayFont != nullptr) {
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayDebug);
m_DebugOverlayFont->DrawTextA(nullptr,
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug),
-1,
&sample.DstRect,
DT_LEFT | DT_NOCLIP,
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
}
}
hr = m_Device->EndScene();
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"EndScene() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
do {
// Use D3DPRESENT_DONOTWAIT if present may block in order to avoid holding the giant
// lock around this D3D device for excessive lengths of time (blocking concurrent decoding tasks).
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, m_BlockingPresent ? D3DPRESENT_DONOTWAIT : 0);
if (hr == D3DERR_WASSTILLDRAWING) {
SDL_Delay(1);
}
} while (hr == D3DERR_WASSTILLDRAWING);
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PresentEx() failed: %x", "PresentEx() failed: %x",
+11 -6
View File
@@ -4,6 +4,7 @@
#include "pacer/pacer.h" #include "pacer/pacer.h"
#include <d3d9.h> #include <d3d9.h>
#include <d3dx9.h>
#include <dxva2api.h> #include <dxva2api.h>
extern "C" { extern "C" {
@@ -20,18 +21,20 @@ public:
int width, int width,
int height, int height,
int maxFps, int maxFps,
bool enableVsync); bool enableVsync) override;
virtual bool prepareDecoderContext(AVCodecContext* context); virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame(); virtual bool needsTestFrame() override;
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities() override;
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint() override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
private: private:
bool initializeDecoder(); bool initializeDecoder();
bool initializeRenderer(); bool initializeRenderer();
bool initializeDevice(SDL_Window* window, bool enableVsync); bool initializeDevice(SDL_Window* window, bool enableVsync);
bool isDecoderBlacklisted(); bool isDecoderBlacklisted();
bool isDXVideoProcessorAPIBlacklisted();
static static
AVBufferRef* ffPoolAlloc(void* opaque, int size); AVBufferRef* ffPoolAlloc(void* opaque, int size);
@@ -67,4 +70,6 @@ private:
DXVA2_ValueRange m_SaturationRange; DXVA2_ValueRange m_SaturationRange;
DXVA2_VideoDesc m_Desc; DXVA2_VideoDesc m_Desc;
REFERENCE_TIME m_FrameIndex; REFERENCE_TIME m_FrameIndex;
LPD3DXFONT m_DebugOverlayFont;
bool m_BlockingPresent;
}; };
@@ -8,11 +8,12 @@
#endif #endif
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <VersionHelpers.h>
#include "dxvsyncsource.h" #include "dxvsyncsource.h"
#endif #endif
#define FRAME_HISTORY_ENTRIES 8
// We may be woken up slightly late so don't go all the way // 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 // up to the next V-sync since we may accidentally step into
// the next V-sync period. It also takes some amount of time // the next V-sync period. It also takes some amount of time
@@ -21,7 +22,8 @@
#define TIMER_SLACK_MS 3 #define TIMER_SLACK_MS 3
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) : Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
m_FrameQueueLock(0), m_RenderThread(nullptr),
m_Stopping(false),
m_VsyncSource(nullptr), m_VsyncSource(nullptr),
m_VsyncRenderer(renderer), m_VsyncRenderer(renderer),
m_MaxVideoFps(0), m_MaxVideoFps(0),
@@ -37,10 +39,70 @@ Pacer::~Pacer()
delete m_VsyncSource; delete m_VsyncSource;
m_VsyncSource = nullptr; m_VsyncSource = nullptr;
while (!m_FrameQueue.isEmpty()) { // Stop the render thread
AVFrame* frame = m_FrameQueue.dequeue(); m_Stopping = true;
if (m_RenderThread != nullptr) {
m_RenderQueueNotEmpty.wakeAll();
SDL_WaitThread(m_RenderThread, nullptr);
}
// Delete any remaining unconsumed frames
while (!m_RenderQueue.isEmpty()) {
AVFrame* frame = m_RenderQueue.dequeue();
av_frame_free(&frame); av_frame_free(&frame);
} }
while (!m_PacingQueue.isEmpty()) {
AVFrame* frame = m_PacingQueue.dequeue();
av_frame_free(&frame);
}
}
int Pacer::renderThread(void* context)
{
Pacer* me = reinterpret_cast<Pacer*>(context);
while (!me->m_Stopping) {
// Acquire the frame queue lock to protect the queue and
// the not empty condition
me->m_FrameQueueLock.lock();
// Wait for a frame to be ready to render
while (!me->m_Stopping && me->m_RenderQueue.isEmpty()) {
me->m_RenderQueueNotEmpty.wait(&me->m_FrameQueueLock);
}
if (me->m_Stopping) {
// Exit this thread
me->m_FrameQueueLock.unlock();
break;
}
// Dequeue the most recent frame for rendering and free the others.
// NB: m_FrameQueueLock still held here!
AVFrame* lastFrame = nullptr;
while (!me->m_RenderQueue.isEmpty()) {
if (lastFrame != nullptr) {
// Don't hold the frame queue lock across av_frame_free(),
// since it could need to talk to the GPU driver. This is safe
// because we're guaranteed that the queue will not shrink during
// this time (and so dequeue() below will always get something).
me->m_FrameQueueLock.unlock();
av_frame_free(&lastFrame);
me->m_VideoStats->pacerDroppedFrames++;
me->m_FrameQueueLock.lock();
}
lastFrame = me->m_RenderQueue.dequeue();
}
// Release the frame queue lock before rendering
me->m_FrameQueueLock.unlock();
// Render and free the mot current frame
me->renderFrame(lastFrame);
}
return 0;
} }
// Called in an arbitrary thread by the IVsyncSource on V-sync // Called in an arbitrary thread by the IVsyncSource on V-sync
@@ -52,9 +114,7 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS); SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS);
Uint32 vsyncCallbackStartTime = SDL_GetTicks(); m_FrameQueueLock.lock();
SDL_AtomicLock(&m_FrameQueueLock);
// If the queue length history entries are large, be strict // If the queue length history entries are large, be strict
// about dropping excess frames. // about dropping excess frames.
@@ -64,8 +124,8 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
// frame history to drop frames only if consistently above the // frame history to drop frames only if consistently above the
// one queued frame mark. // one queued frame mark.
if (m_MaxVideoFps >= m_DisplayFps) { if (m_MaxVideoFps >= m_DisplayFps) {
for (int i = 0; i < m_FrameQueueHistory.count(); i++) { for (int queueHistoryEntry : m_PacingQueueHistory) {
if (m_FrameQueueHistory[i] <= 1) { if (queueHistoryEntry <= 1) {
// Be lenient as long as the queue length // Be lenient as long as the queue length
// resolves before the end of frame history // resolves before the end of frame history
frameDropTarget = 3; frameDropTarget = 3;
@@ -73,70 +133,58 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
} }
} }
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) { // Keep a rolling 500 ms window of pacing queue history
m_FrameQueueHistory.dequeue(); if (m_PacingQueueHistory.count() == m_DisplayFps / 2) {
m_PacingQueueHistory.dequeue();
} }
m_FrameQueueHistory.enqueue(m_FrameQueue.count()); m_PacingQueueHistory.enqueue(m_PacingQueue.count());
} }
// Catch up if we're several frames ahead // Catch up if we're several frames ahead
while (m_FrameQueue.count() > frameDropTarget) { while (m_PacingQueue.count() > frameDropTarget) {
AVFrame* frame = m_FrameQueue.dequeue(); AVFrame* frame = m_PacingQueue.dequeue();
// Drop the lock while we call av_frame_free()
m_FrameQueueLock.unlock();
m_VideoStats->pacerDroppedFrames++; m_VideoStats->pacerDroppedFrames++;
av_frame_free(&frame); av_frame_free(&frame);
m_FrameQueueLock.lock();
} }
if (m_PacingQueue.isEmpty()) {
if (m_FrameQueue.isEmpty()) { // Wait for a frame to arrive or our V-sync timeout to expire
SDL_AtomicUnlock(&m_FrameQueueLock); if (!m_PacingQueueNotEmpty.wait(&m_FrameQueueLock, timeUntilNextVsyncMillis - TIMER_SLACK_MS)) {
// Wait timed out - unlock and bail
while (!SDL_TICKS_PASSED(SDL_GetTicks(), m_FrameQueueLock.unlock();
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; return;
} }
RenderNextFrame:
// Grab the first frame
AVFrame* frame = m_FrameQueue.dequeue();
SDL_AtomicUnlock(&m_FrameQueueLock);
// Render it
Uint32 beforeRender = SDL_GetTicks();
m_VsyncRenderer->renderFrameAtVsync(frame);
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
m_VideoStats->renderedFrames++;
// Free the frame
av_frame_free(&frame);
} }
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync) // Place the first frame on the render queue
m_RenderQueue.enqueue(m_PacingQueue.dequeue());
m_FrameQueueLock.unlock();
m_RenderQueueNotEmpty.wakeOne();
}
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
{ {
m_MaxVideoFps = maxVideoFps; m_MaxVideoFps = maxVideoFps;
m_EnableVsync = enableVsync;
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window); m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
if (m_EnableVsync) { if (enablePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing in tear-free mode: target %d Hz with %d FPS stream", "Frame pacing active: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps); m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN) #if defined(Q_OS_DARWIN)
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this); m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
#elif defined(Q_OS_WIN32) #elif defined(Q_OS_WIN32)
// Don't use D3DKMTWaitForVerticalBlankEvent() on Windows 7, because
// it blocks during other concurrent DX operations (like actually rendering).
if (IsWindows8OrGreater()) {
m_VsyncSource = new DxVsyncSource(this); m_VsyncSource = new DxVsyncSource(this);
}
#else #else
// Platforms without a VsyncSource will just render frames // Platforms without a VsyncSource will just render frames
// immediately like they used to. // immediately like they used to.
@@ -148,36 +196,81 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
} }
else { else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Minimal latency tearing mode: target %d Hz with %d FPS stream", "Frame pacing disabled: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps); m_DisplayFps, m_MaxVideoFps);
} }
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "Pacer Render Thread", this);
return true; return true;
} }
void Pacer::renderFrame(AVFrame* frame)
{
// Count time spent in Pacer's queues
Uint32 beforeRender = SDL_GetTicks();
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
// Render it
m_VsyncRenderer->renderFrame(frame);
Uint32 afterRender = SDL_GetTicks();
m_VideoStats->totalRenderTime += afterRender - beforeRender;
m_VideoStats->renderedFrames++;
av_frame_free(&frame);
// Drop frames if we have too many queued up for a while
m_FrameQueueLock.lock();
int frameDropTarget = 0;
for (int queueHistoryEntry : m_RenderQueueHistory) {
if (queueHistoryEntry == 0) {
// Be lenient as long as the queue length
// resolves before the end of frame history
frameDropTarget = 2;
break;
}
}
// Keep a rolling 500 ms window of render queue history
if (m_RenderQueueHistory.count() == m_MaxVideoFps / 2) {
m_RenderQueueHistory.dequeue();
}
m_RenderQueueHistory.enqueue(m_RenderQueue.count());
// Catch up if we're several frames ahead
while (m_RenderQueue.count() > frameDropTarget) {
AVFrame* frame = m_RenderQueue.dequeue();
// Drop the lock while we call av_frame_free()
m_FrameQueueLock.unlock();
m_VideoStats->pacerDroppedFrames++;
av_frame_free(&frame);
m_FrameQueueLock.lock();
}
m_FrameQueueLock.unlock();
}
void Pacer::submitFrame(AVFrame* frame) void Pacer::submitFrame(AVFrame* frame)
{ {
// Make sure initialize() has been called // Make sure initialize() has been called
SDL_assert(m_MaxVideoFps != 0); SDL_assert(m_MaxVideoFps != 0);
// Queue the frame until the V-sync callback if // Queue the frame and possibly wake up the render thread
// we have a V-sync source, otherwise deliver it m_FrameQueueLock.lock();
// immediately and hope for the best. if (m_VsyncSource != nullptr) {
if (isUsingFrameQueue()) { m_PacingQueue.enqueue(frame);
SDL_AtomicLock(&m_FrameQueueLock);
m_FrameQueue.enqueue(frame);
SDL_AtomicUnlock(&m_FrameQueueLock);
} }
else { else {
Uint32 beforeRender = SDL_GetTicks(); m_RenderQueue.enqueue(frame);
m_VsyncRenderer->renderFrameAtVsync(frame); }
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender; m_FrameQueueLock.unlock();
m_VideoStats->renderedFrames++; if (m_VsyncSource != nullptr) {
av_frame_free(&frame); m_PacingQueueNotEmpty.wakeOne();
}
else {
m_RenderQueueNotEmpty.wakeOne();
} }
} }
bool Pacer::isUsingFrameQueue()
{
return m_VsyncSource != nullptr;
}
@@ -4,6 +4,8 @@
#include "../renderer.h" #include "../renderer.h"
#include <QQueue> #include <QQueue>
#include <QMutex>
#include <QWaitCondition>
class IVsyncSource { class IVsyncSource {
public: public:
@@ -20,21 +22,28 @@ public:
void submitFrame(AVFrame* frame); void submitFrame(AVFrame* frame);
bool initialize(SDL_Window* window, int maxVideoFps, bool enableVsync); bool initialize(SDL_Window* window, int maxVideoFps, bool enablePacing);
void vsyncCallback(int timeUntilNextVsyncMillis); void vsyncCallback(int timeUntilNextVsyncMillis);
bool isUsingFrameQueue();
private: private:
QQueue<AVFrame*> m_FrameQueue; static int renderThread(void* context);
QQueue<int> m_FrameQueueHistory;
SDL_SpinLock m_FrameQueueLock; void renderFrame(AVFrame* frame);
QQueue<AVFrame*> m_RenderQueue;
QQueue<AVFrame*> m_PacingQueue;
QQueue<int> m_PacingQueueHistory;
QQueue<int> m_RenderQueueHistory;
QMutex m_FrameQueueLock;
QWaitCondition m_RenderQueueNotEmpty;
QWaitCondition m_PacingQueueNotEmpty;
SDL_Thread* m_RenderThread;
bool m_Stopping;
IVsyncSource* m_VsyncSource; IVsyncSource* m_VsyncSource;
IFFmpegRenderer* m_VsyncRenderer; IFFmpegRenderer* m_VsyncRenderer;
int m_MaxVideoFps; int m_MaxVideoFps;
int m_DisplayFps; int m_DisplayFps;
bool m_EnableVsync;
PVIDEO_STATS m_VideoStats; PVIDEO_STATS m_VideoStats;
}; };
+13 -28
View File
@@ -2,19 +2,20 @@
#include <SDL.h> #include <SDL.h>
#include "streaming/video/overlaymanager.h"
extern "C" { extern "C" {
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
} }
class IFFmpegRenderer { class IFFmpegRenderer : public Overlay::IOverlayRenderer {
public: public:
enum VSyncConstraint { enum FramePacingConstraint {
VSYNC_FORCE_OFF, PACING_FORCE_OFF,
VSYNC_FORCE_ON, PACING_FORCE_ON,
VSYNC_ANY PACING_ANY
}; };
virtual ~IFFmpegRenderer() {}
virtual bool initialize(SDL_Window* window, virtual bool initialize(SDL_Window* window,
int videoFormat, int videoFormat,
int width, int width,
@@ -22,29 +23,13 @@ public:
int maxFps, int maxFps,
bool enableVsync) = 0; bool enableVsync) = 0;
virtual bool prepareDecoderContext(AVCodecContext* context) = 0; virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
virtual void renderFrameAtVsync(AVFrame* frame) = 0; virtual void renderFrame(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0; virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0; virtual int getDecoderCapabilities() = 0;
virtual VSyncConstraint getVsyncConstraint() = 0; virtual FramePacingConstraint getFramePacingConstraint() = 0;
};
class SdlRenderer : public IFFmpegRenderer { // IOverlayRenderer
public: virtual void notifyOverlayUpdated(Overlay::OverlayType) override {
SdlRenderer(); // Nothing
virtual ~SdlRenderer(); }
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:
SDL_Renderer* m_Renderer;
SDL_Texture* m_Texture;
}; };
+108 -6
View File
@@ -1,16 +1,45 @@
#include "renderer.h" #include "sdlvid.h"
#include "streaming/session.h"
#include "path.h"
#include <QDir>
#include <Limelight.h> #include <Limelight.h>
SdlRenderer::SdlRenderer() SdlRenderer::SdlRenderer()
: m_Renderer(nullptr), : m_Renderer(nullptr),
m_Texture(nullptr) m_Texture(nullptr),
m_DebugOverlayFont(nullptr),
m_DebugOverlaySurface(nullptr),
m_DebugOverlayTexture(nullptr)
{ {
SDL_assert(TTF_WasInit() == 0);
if (TTF_Init() != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"TTF_Init() failed: %s",
TTF_GetError());
return;
}
} }
SdlRenderer::~SdlRenderer() SdlRenderer::~SdlRenderer()
{ {
if (m_DebugOverlayFont != nullptr) {
TTF_CloseFont(m_DebugOverlayFont);
}
TTF_Quit();
SDL_assert(TTF_WasInit() == 0);
if (m_DebugOverlayTexture != nullptr) {
SDL_DestroyTexture(m_DebugOverlayTexture);
}
if (m_DebugOverlaySurface != nullptr) {
SDL_FreeSurface(m_DebugOverlaySurface);
}
if (m_Texture != nullptr) { if (m_Texture != nullptr) {
SDL_DestroyTexture(m_Texture); SDL_DestroyTexture(m_Texture);
} }
@@ -43,9 +72,53 @@ int SdlRenderer::getDecoderCapabilities()
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC; return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
} }
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint SdlRenderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
}
void SdlRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
{
if (type == Overlay::OverlayDebug) {
if (m_DebugOverlayFont == nullptr) {
QByteArray fontPath = QDir::toNativeSeparators(Path::getDataFilePath("ModeSeven.ttf")).toUtf8();
if (fontPath.isEmpty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to locate SDL overlay font");
return;
}
m_DebugOverlayFont = TTF_OpenFont(fontPath.data(),
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug));
if (m_DebugOverlayFont == nullptr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"TTF_OpenFont() failed: %s",
TTF_GetError());
// Can't proceed without a font
return;
}
}
SDL_Surface* oldSurface = (SDL_Surface*)SDL_AtomicGetPtr((void**)&m_DebugOverlaySurface);
// Free the old surface
if (oldSurface != nullptr && SDL_AtomicCASPtr((void**)&m_DebugOverlaySurface, oldSurface, nullptr)) {
SDL_FreeSurface(oldSurface);
}
if (Session::get()->getOverlayManager().isOverlayEnabled(type)) {
// The _Wrapped variant is required for line breaks to work
SDL_Surface* surface = TTF_RenderText_Blended_Wrapped(m_DebugOverlayFont,
Session::get()->getOverlayManager().getOverlayText(type),
Session::get()->getOverlayManager().getOverlayColor(type),
1000);
SDL_AtomicSetPtr((void**)&m_DebugOverlaySurface, surface);
}
}
else {
SDL_assert(false);
}
} }
bool SdlRenderer::initialize(SDL_Window* window, bool SdlRenderer::initialize(SDL_Window* window,
@@ -98,7 +171,7 @@ bool SdlRenderer::initialize(SDL_Window* window,
return true; return true;
} }
void SdlRenderer::renderFrameAtVsync(AVFrame* frame) void SdlRenderer::renderFrame(AVFrame* frame)
{ {
SDL_UpdateYUVTexture(m_Texture, nullptr, SDL_UpdateYUVTexture(m_Texture, nullptr,
frame->data[0], frame->data[0],
@@ -109,6 +182,35 @@ void SdlRenderer::renderFrameAtVsync(AVFrame* frame)
frame->linesize[2]); frame->linesize[2]);
SDL_RenderClear(m_Renderer); SDL_RenderClear(m_Renderer);
// Draw the video content itself
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr); SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
// Draw the overlays
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
// If a new surface has been created for updated overlay data, convert it into a texture.
// NB: We have to do this conversion at render-time because we can only interact
// with the renderer on a single thread.
SDL_Surface* surface = (SDL_Surface*)SDL_AtomicGetPtr((void**)&m_DebugOverlaySurface);
if (surface != nullptr && SDL_AtomicCASPtr((void**)&m_DebugOverlaySurface, surface, nullptr)) {
if (m_DebugOverlayTexture != nullptr) {
SDL_DestroyTexture(m_DebugOverlayTexture);
}
m_DebugOverlayRect.x = 0;
m_DebugOverlayRect.y = 0;
m_DebugOverlayRect.w = surface->w;
m_DebugOverlayRect.h = surface->h;
m_DebugOverlayTexture = SDL_CreateTextureFromSurface(m_Renderer, surface);
SDL_FreeSurface(surface);
}
// If we have a debug overlay texture, render it too
if (m_DebugOverlayTexture != nullptr) {
SDL_RenderCopy(m_Renderer, m_DebugOverlayTexture, nullptr, &m_DebugOverlayRect);
}
}
SDL_RenderPresent(m_Renderer); SDL_RenderPresent(m_Renderer);
} }
@@ -0,0 +1,32 @@
#pragma once
#include "renderer.h"
#include <SDL_ttf.h>
class SdlRenderer : public IFFmpegRenderer {
public:
SdlRenderer();
virtual ~SdlRenderer() override;
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual int getDecoderCapabilities() override;
virtual FramePacingConstraint getFramePacingConstraint() override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
private:
SDL_Renderer* m_Renderer;
SDL_Texture* m_Texture;
TTF_Font* m_DebugOverlayFont;
SDL_Surface* m_DebugOverlaySurface;
SDL_Texture* m_DebugOverlayTexture;
SDL_Rect m_DebugOverlayRect;
};
@@ -177,13 +177,13 @@ VAAPIRenderer::getDecoderCapabilities()
return 0; return 0;
} }
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint VAAPIRenderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
} }
void void
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame) VAAPIRenderer::renderFrame(AVFrame* frame)
{ {
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3]; VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data; AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
+2 -2
View File
@@ -37,10 +37,10 @@ public:
int maxFps, int maxFps,
bool enableVsync); bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context); virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrame(AVFrame* frame);
virtual bool needsTestFrame(); virtual bool needsTestFrame();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint();
private: private:
int m_WindowSystem; int m_WindowSystem;
@@ -246,12 +246,12 @@ int VDPAURenderer::getDecoderCapabilities()
return 0; return 0;
} }
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint VDPAURenderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
} }
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame) void VDPAURenderer::renderFrame(AVFrame* frame)
{ {
VdpStatus status; VdpStatus status;
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3]; VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
@@ -335,8 +335,8 @@ void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
0, nullptr, 0, nullptr,
&sourceRect, &sourceRect,
chosenSurface, chosenSurface,
&outputRect,
nullptr, nullptr,
&outputRect,
0, 0,
nullptr); nullptr);
if (status != VDP_STATUS_OK) { if (status != VDP_STATUS_OK) {
+2 -2
View File
@@ -20,10 +20,10 @@ public:
int maxFps, int maxFps,
bool enableVsync); bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context); virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrame(AVFrame* frame);
virtual bool needsTestFrame(); virtual bool needsTestFrame();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint();
private: private:
uint32_t m_VideoWidth, m_VideoHeight; uint32_t m_VideoWidth, m_VideoHeight;
+61 -15
View File
@@ -7,11 +7,13 @@
#include <SDL_syswm.h> #include <SDL_syswm.h>
#include <Limelight.h> #include <Limelight.h>
#include <streaming/session.h>
#include <mach/mach_time.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>
#import <dispatch/dispatch.h>
class VTRenderer : public IFFmpegRenderer class VTRenderer : public IFFmpegRenderer
{ {
@@ -20,7 +22,8 @@ public:
: m_HwContext(nullptr), : m_HwContext(nullptr),
m_DisplayLayer(nullptr), m_DisplayLayer(nullptr),
m_FormatDesc(nullptr), m_FormatDesc(nullptr),
m_View(nullptr) m_StreamView(nullptr),
m_DebugOverlayTextField(nullptr)
{ {
} }
@@ -34,13 +37,17 @@ public:
CFRelease(m_FormatDesc); CFRelease(m_FormatDesc);
} }
if (m_View != nullptr) { if (m_DebugOverlayTextField != nullptr) {
[m_View removeFromSuperview]; [m_DebugOverlayTextField removeFromSuperview];
}
if (m_StreamView != nullptr) {
[m_StreamView removeFromSuperview];
} }
} }
// Caller frees frame after we return // Caller frees frame after we return
virtual void renderFrameAtVsync(AVFrame* frame) override virtual void renderFrame(AVFrame* frame) override
{ {
OSStatus status; OSStatus status;
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]); CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
@@ -159,21 +166,21 @@ public:
// SDL adds its own content view to listen for events. // SDL adds its own content view to listen for events.
// We need to add a subview for our display layer. // We need to add a subview for our display layer.
NSView* contentView = info.info.cocoa.window.contentView; NSView* contentView = info.info.cocoa.window.contentView;
m_View = [[NSView alloc] initWithFrame:contentView.bounds]; m_StreamView = [[NSView alloc] initWithFrame:contentView.bounds];
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init]; m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
m_DisplayLayer.bounds = m_View.bounds; m_DisplayLayer.bounds = m_StreamView.bounds;
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds)); m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_StreamView.bounds), CGRectGetMidY(m_StreamView.bounds));
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect; m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
// Create a layer-hosted view by setting the layer before wantsLayer // Create a layer-hosted view by setting the layer before wantsLayer
// This avoids us having to add our AVSampleBufferDisplayLayer as a // This avoids us having to add our AVSampleBufferDisplayLayer as a
// sublayer of a layer-backed view which leaves a useless layer in // sublayer of a layer-backed view which leaves a useless layer in
// the middle. // the middle.
m_View.layer = m_DisplayLayer; m_StreamView.layer = m_DisplayLayer;
m_View.wantsLayer = YES; m_StreamView.wantsLayer = YES;
[contentView addSubview: m_View]; [contentView addSubview: m_StreamView];
err = av_hwdevice_ctx_create(&m_HwContext, err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VIDEOTOOLBOX, AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
@@ -190,6 +197,41 @@ public:
return true; return true;
} }
static void updateDebugOverlayOnMainThread(void* context)
{
VTRenderer* me = (VTRenderer*)context;
// Lazy initialization for the overlay
if (me->m_DebugOverlayTextField == nullptr) {
me->m_DebugOverlayTextField = [[NSTextField alloc] initWithFrame:me->m_StreamView.bounds];
[me->m_DebugOverlayTextField setBezeled:NO];
[me->m_DebugOverlayTextField setDrawsBackground:NO];
[me->m_DebugOverlayTextField setEditable:NO];
[me->m_DebugOverlayTextField setSelectable:NO];
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayDebug);
[me->m_DebugOverlayTextField setTextColor:[NSColor colorWithSRGBRed:color.r / 255.0 green:color.g / 255.0 blue:color.b / 255.0 alpha:color.a / 255.0]];
[me->m_DebugOverlayTextField setFont:[NSFont messageFontOfSize:Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug)]];
[me->m_StreamView addSubview: me->m_DebugOverlayTextField];
}
// Update text contents
[me->m_DebugOverlayTextField setStringValue: [NSString stringWithUTF8String:Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug)]];
// Unhide if it's enabled
[me->m_DebugOverlayTextField setHidden: !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)];
}
virtual void notifyOverlayUpdated(Overlay::OverlayType type) override
{
// We must do the actual UI updates on the main thread, so queue an
// async callback on the main thread via GCD to do the UI update.
if (type == Overlay::OverlayDebug) {
dispatch_async_f(dispatch_get_main_queue(), this, updateDebugOverlayOnMainThread);
}
}
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);
@@ -202,8 +244,11 @@ public:
virtual bool needsTestFrame() override virtual bool needsTestFrame() override
{ {
// We query VT to determine whether the codec is supported // We used to trust VT to tell us whether decode will work, but
return false; // there are cases where it can lie because the hardware technically
// can decode the format but VT is unserviceable for some other reason.
// Decoding the test frame will tell us for sure whether it will work.
return true;
} }
virtual int getDecoderCapabilities() override virtual int getDecoderCapabilities() override
@@ -211,19 +256,20 @@ public:
return 0; return 0;
} }
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override virtual IFFmpegRenderer::FramePacingConstraint getFramePacingConstraint() override
{ {
// This renderer is inherently tied to V-sync due how we're // This renderer is inherently tied to V-sync due how we're
// rendering with AVSampleBufferDisplay layer. Running without // rendering with AVSampleBufferDisplay layer. Running without
// the V-Sync source leads to massive stuttering. // the V-Sync source leads to massive stuttering.
return VSYNC_FORCE_ON; return PACING_FORCE_ON;
} }
private: private:
AVBufferRef* m_HwContext; AVBufferRef* m_HwContext;
AVSampleBufferDisplayLayer* m_DisplayLayer; AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc; CMVideoFormatDescriptionRef m_FormatDesc;
NSView* m_View; NSView* m_StreamView;
NSTextField* m_DebugOverlayTextField;
}; };
IFFmpegRenderer* VTRendererFactory::createRenderer() { IFFmpegRenderer* VTRendererFactory::createRenderer() {
+132 -126
View File
@@ -1,8 +1,12 @@
#include <Limelight.h> #include <Limelight.h>
#include "ffmpeg.h" #include "ffmpeg.h"
#include "streaming/streamutils.h" #include "streaming/streamutils.h"
#include "streaming/session.h"
#include <h264_stream.h> #include <h264_stream.h>
#include "ffmpeg-renderers/sdlvid.h"
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
#include "ffmpeg-renderers/dxva2.h" #include "ffmpeg-renderers/dxva2.h"
#endif #endif
@@ -33,7 +37,7 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
int FFmpegVideoDecoder::getDecoderCapabilities() int FFmpegVideoDecoder::getDecoderCapabilities()
{ {
return m_Renderer->getDecoderCapabilities(); return CAPABILITY_DIRECT_SUBMIT | m_Renderer->getDecoderCapabilities();
} }
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context, enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
@@ -57,7 +61,7 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
return AV_PIX_FMT_NONE; return AV_PIX_FMT_NONE;
} }
FFmpegVideoDecoder::FFmpegVideoDecoder() FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
: m_VideoDecoderCtx(nullptr), : m_VideoDecoderCtx(nullptr),
m_DecodeBuffer(1024 * 1024, 0), m_DecodeBuffer(1024 * 1024, 0),
m_HwDecodeCfg(nullptr), m_HwDecodeCfg(nullptr),
@@ -66,10 +70,10 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
m_Pacer(nullptr), m_Pacer(nullptr),
m_LastFrameNumber(0), m_LastFrameNumber(0),
m_StreamFps(0), m_StreamFps(0),
m_NeedsSpsFixup(false) m_NeedsSpsFixup(false),
m_TestOnly(testOnly)
{ {
av_init_packet(&m_Pkt); av_init_packet(&m_Pkt);
SDL_AtomicSet(&m_QueuedFrames, 0);
SDL_zero(m_ActiveWndVideoStats); SDL_zero(m_ActiveWndVideoStats);
SDL_zero(m_LastWndVideoStats); SDL_zero(m_LastWndVideoStats);
@@ -82,6 +86,12 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
FFmpegVideoDecoder::~FFmpegVideoDecoder() FFmpegVideoDecoder::~FFmpegVideoDecoder()
{ {
reset(); reset();
// Set log level back to default.
// NB: We don't do this in reset() because we want
// to preserve the log level across reset() during
// test initialization.
av_log_set_level(AV_LOG_INFO);
} }
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer() IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
@@ -91,28 +101,6 @@ IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
void FFmpegVideoDecoder::reset() 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; delete m_Pacer;
m_Pacer = nullptr; m_Pacer = nullptr;
@@ -123,37 +111,54 @@ void FFmpegVideoDecoder::reset()
// need to delete in the renderer destructor. // need to delete in the renderer destructor.
avcodec_free_context(&m_VideoDecoderCtx); avcodec_free_context(&m_VideoDecoderCtx);
if (!m_TestOnly) {
Session::get()->getOverlayManager().setOverlayRenderer(nullptr);
}
delete m_Renderer; delete m_Renderer;
m_Renderer = nullptr; m_Renderer = nullptr;
if (!m_TestOnly) {
logVideoStats(m_GlobalVideoStats, "Global video stats"); logVideoStats(m_GlobalVideoStats, "Global video stats");
} }
else {
// Test-only decoders can't have any frames submitted
SDL_assert(m_GlobalVideoStats.totalFrames == 0);
}
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window, bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height, int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly) int maxFps, bool enableFramePacing, bool testFrame)
{ {
auto vsyncConstraint = m_Renderer->getVsyncConstraint(); // In test-only mode, we should only see test frames
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) { SDL_assert(!m_TestOnly || testFrame);
auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully disabled by the active renderer"); "Frame pacing is forcefully disabled by the active renderer");
enableVsync = false; enableFramePacing = false;
} }
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) { else if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_ON && !enableFramePacing) {
// FIXME: This duplicates logic in Session.cpp // FIXME: This duplicates logic in Session.cpp
int displayHz = StreamUtils::getDisplayRefreshRate(window); int displayHz = StreamUtils::getDisplayRefreshRate(window);
if (displayHz + 5 >= maxFps) { if (displayHz + 5 >= maxFps) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully enabled by the active renderer"); "Frame pacing is forcefully enabled by the active renderer");
enableVsync = true; enableFramePacing = true;
} }
} }
m_StreamFps = maxFps; m_StreamFps = maxFps;
// Don't bother initializing Pacer if we're not actually going to render
if (!testFrame) {
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats); m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(window, maxFps, enableVsync)) { if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
return false; return false;
} }
}
m_VideoDecoderCtx = avcodec_alloc_context3(decoder); m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
if (!m_VideoDecoderCtx) { if (!m_VideoDecoderCtx) {
@@ -210,7 +215,7 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
// our minds on the selected video codec, so we'll do a trial run // 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 // now to see if things will actually work when the video stream
// comes in. // comes in.
if (testOnly) { if (testFrame) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) { if (videoFormat & VIDEO_FORMAT_MASK_H264) {
m_Pkt.data = (uint8_t*)k_H264TestFrame; m_Pkt.data = (uint8_t*)k_H264TestFrame;
m_Pkt.size = sizeof(k_H264TestFrame); m_Pkt.size = sizeof(k_H264TestFrame);
@@ -239,12 +244,10 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
else { else {
m_NeedsSpsFixup = false; m_NeedsSpsFixup = false;
} }
}
#ifdef QT_DEBUG // Tell overlay manager to use this renderer
// Restore default log level before streaming Session::get()->getOverlayManager().setOverlayRenderer(m_Renderer);
av_log_set_level(AV_LOG_INFO); }
#endif
return true; return true;
} }
@@ -254,10 +257,12 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
dst.receivedFrames += src.receivedFrames; dst.receivedFrames += src.receivedFrames;
dst.decodedFrames += src.decodedFrames; dst.decodedFrames += src.decodedFrames;
dst.renderedFrames += src.renderedFrames; dst.renderedFrames += src.renderedFrames;
dst.totalFrames += src.totalFrames;
dst.networkDroppedFrames += src.networkDroppedFrames; dst.networkDroppedFrames += src.networkDroppedFrames;
dst.pacerDroppedFrames += src.pacerDroppedFrames; dst.pacerDroppedFrames += src.pacerDroppedFrames;
dst.totalReassemblyTime += src.totalReassemblyTime; dst.totalReassemblyTime += src.totalReassemblyTime;
dst.totalDecodeTime += src.totalDecodeTime; dst.totalDecodeTime += src.totalDecodeTime;
dst.totalPacerTime += src.totalPacerTime;
dst.totalRenderTime += src.totalRenderTime; dst.totalRenderTime += src.totalRenderTime;
Uint32 now = SDL_GetTicks(); Uint32 now = SDL_GetTicks();
@@ -270,56 +275,59 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
// The following code assumes the global measure was already started first // The following code assumes the global measure was already started first
SDL_assert(dst.measurementStartTimestamp <= src.measurementStartTimestamp); SDL_assert(dst.measurementStartTimestamp <= src.measurementStartTimestamp);
dst.totalFps = (float)dst.totalFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
dst.receivedFps = (float)dst.receivedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000); dst.receivedFps = (float)dst.receivedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
dst.decodedFps = (float)dst.decodedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000); dst.decodedFps = (float)dst.decodedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
dst.renderedFps = (float)dst.renderedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000); dst.renderedFps = (float)dst.renderedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
} }
void FFmpegVideoDecoder::stringifyVideoStats(VIDEO_STATS& stats, char* output)
{
int offset = 0;
// Start with an empty string
output[offset] = 0;
if (stats.receivedFps > 0) {
offset += sprintf(&output[offset],
"Estimated host PC frame rate: %.2f FPS\n"
"Incoming frame rate from network: %.2f FPS\n"
"Decoding frame rate: %.2f FPS\n"
"Rendering frame rate: %.2f FPS\n",
stats.totalFps,
stats.receivedFps,
stats.decodedFps,
stats.renderedFps);
}
if (stats.renderedFrames != 0) {
offset += sprintf(&output[offset],
"Frames dropped by your network connection: %.2f%%\n"
"Frames dropped due to network jitter: %.2f%%\n"
"Average receive time: %.2f ms\n"
"Average decoding time: %.2f ms\n"
"Average frame queue delay: %.2f ms\n"
"Average rendering time (including monitor V-sync latency): %.2f ms\n",
(float)stats.networkDroppedFrames / stats.totalFrames * 100,
(float)stats.pacerDroppedFrames / stats.decodedFrames * 100,
(float)stats.totalReassemblyTime / stats.receivedFrames,
(float)stats.totalDecodeTime / stats.decodedFrames,
(float)stats.totalPacerTime / stats.renderedFrames,
(float)stats.totalRenderTime / stats.renderedFrames);
}
}
void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title) void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
{ {
if (stats.renderedFps > 0 || stats.renderedFrames != 0) { if (stats.renderedFps > 0 || stats.renderedFrames != 0) {
char videoStatsStr[512];
stringifyVideoStats(stats, videoStatsStr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"%s", title); "%s", title);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"----------------------------------------------------------"); "----------------------------------------------------------\n%s",
} videoStatsStr);
if (stats.renderedFps > 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Incoming frame rate from network: %.2f FPS",
stats.receivedFps);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Decoding frame rate: %.2f FPS",
stats.decodedFps);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Rendering frame rate: %.2f FPS",
stats.renderedFps);
}
if (stats.renderedFrames != 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Total frames received: %u",
stats.receivedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Total frames decoded: %u",
stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Total frames rendered: %u",
stats.renderedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average reassembly time: %.2f ms",
(float)stats.totalReassemblyTime / stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average decode time: %.2f ms",
(float)stats.totalDecodeTime / stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average render time: %.2f ms",
(float)stats.totalRenderTime / stats.renderedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frames lost during network transmission: %.2f%%",
(float)stats.networkDroppedFrames / stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frames dropped by frame pacing: %.2f%%",
(float)stats.pacerDroppedFrames / stats.decodedFrames);
} }
} }
@@ -359,14 +367,13 @@ bool FFmpegVideoDecoder::initialize(
int width, int width,
int height, int height,
int maxFps, int maxFps,
bool enableVsync) bool enableVsync,
bool enableFramePacing)
{ {
AVCodec* decoder; AVCodec* decoder;
#ifdef QT_DEBUG // Increase log level until the first frame is decoded
// Increase log level during initialization
av_log_set_level(AV_LOG_DEBUG); av_log_set_level(AV_LOG_DEBUG);
#endif
if (videoFormat & VIDEO_FORMAT_MASK_H264) { if (videoFormat & VIDEO_FORMAT_MASK_H264) {
decoder = avcodec_find_decoder(AV_CODEC_ID_H264); decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
@@ -395,7 +402,7 @@ bool FFmpegVideoDecoder::initialize(
m_Renderer = new SdlRenderer(); m_Renderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE && if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) && m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) { completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly)) {
return true; return true;
} }
else { else {
@@ -412,13 +419,19 @@ bool FFmpegVideoDecoder::initialize(
m_HwDecodeCfg = config; m_HwDecodeCfg = config;
// Initialize the hardware codec and submit a test frame if the renderer needs it // Initialize the hardware codec and submit a test frame if the renderer needs it
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) && if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, m_Renderer->needsTestFrame())) { completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly || m_Renderer->needsTestFrame())) {
if (m_TestOnly) {
// This decoder is only for testing capabilities, so don't bother
// creating a usable renderer
return true;
}
if (m_Renderer->needsTestFrame()) { if (m_Renderer->needsTestFrame()) {
// The test worked, so now let's initialize it for real // The test worked, so now let's initialize it for real
reset(); reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr && if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) && m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) { completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
return true; return true;
} }
else { else {
@@ -487,16 +500,30 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
PLENTRY entry = du->bufferList; PLENTRY entry = du->bufferList;
int err; int err;
SDL_assert(!m_TestOnly);
if (!m_LastFrameNumber) {
m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks();
m_LastFrameNumber = du->frameNumber;
}
else {
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
m_ActiveWndVideoStats.networkDroppedFrames += du->frameNumber - (m_LastFrameNumber + 1);
m_ActiveWndVideoStats.totalFrames += du->frameNumber - (m_LastFrameNumber + 1);
m_LastFrameNumber = du->frameNumber;
}
// Flip stats windows roughly every second // Flip stats windows roughly every second
if (m_ActiveWndVideoStats.receivedFrames == m_StreamFps) { if (SDL_TICKS_PASSED(SDL_GetTicks(), m_ActiveWndVideoStats.measurementStartTimestamp + 1000)) {
#if 0 // Update overlay stats if it's enabled
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
VIDEO_STATS lastTwoWndStats = {}; VIDEO_STATS lastTwoWndStats = {};
addVideoStats(m_LastWndVideoStats, lastTwoWndStats); addVideoStats(m_LastWndVideoStats, lastTwoWndStats);
addVideoStats(m_ActiveWndVideoStats, lastTwoWndStats); addVideoStats(m_ActiveWndVideoStats, lastTwoWndStats);
// Print stats from the last 2 windows stringifyVideoStats(lastTwoWndStats, Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug));
logVideoStats(lastTwoWndStats, "Periodic video stats"); Session::get()->getOverlayManager().setOverlayTextUpdated(Overlay::OverlayDebug);
#endif }
// Accumulate these values into the global stats // Accumulate these values into the global stats
addVideoStats(m_ActiveWndVideoStats, m_GlobalVideoStats); addVideoStats(m_ActiveWndVideoStats, m_GlobalVideoStats);
@@ -508,16 +535,7 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
} }
m_ActiveWndVideoStats.receivedFrames++; m_ActiveWndVideoStats.receivedFrames++;
m_ActiveWndVideoStats.totalFrames++;
if (!m_LastFrameNumber) {
m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks();
m_LastFrameNumber = du->frameNumber;
}
else {
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
m_ActiveWndVideoStats.networkDroppedFrames += du->frameNumber - (m_LastFrameNumber + 1);
m_LastFrameNumber = du->frameNumber;
}
int requiredBufferSize = du->fullLength; int requiredBufferSize = du->fullLength;
if (du->frameType == FRAME_TYPE_IDR) { if (du->frameType == FRAME_TYPE_IDR) {
@@ -577,14 +595,19 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// Reset failed decodes count if we reached this far // Reset failed decodes count if we reached this far
m_ConsecutiveFailedDecodes = 0; m_ConsecutiveFailedDecodes = 0;
// Queue the frame for rendering from the main thread // Restore default log level after a successful decode
SDL_AtomicIncRef(&m_QueuedFrames); av_log_set_level(AV_LOG_INFO);
queueFrame(frame);
// Capture a frame timestamp to measuring pacing delay
frame->pts = SDL_GetTicks();
// Count time in avcodec_send_packet() and avcodec_receive_frame() // Count time in avcodec_send_packet() and avcodec_receive_frame()
// as time spent decoding // as time spent decoding
m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode; m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode;
m_ActiveWndVideoStats.decodedFrames++; m_ActiveWndVideoStats.decodedFrames++;
// Queue the frame for rendering (or render now if pacer is disabled)
m_Pacer->submitFrame(frame);
} }
else { else {
av_frame_free(&frame); av_frame_free(&frame);
@@ -608,30 +631,13 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
} }
// Called on main thread // Called on main thread
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event) void FFmpegVideoDecoder::renderFrame(SDL_UserEvent*)
{ {
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1); SDL_assert(false);
m_Pacer->submitFrame(frame);
SDL_AtomicDecRef(&m_QueuedFrames);
} }
// Called on main thread // Called on main thread
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event) void FFmpegVideoDecoder::dropFrame(SDL_UserEvent*)
{ {
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1); SDL_assert(false);
// 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);
// Since Pacer won't see this frame, we'll mark it as a drop
// on its behalf
m_ActiveWndVideoStats.pacerDroppedFrames++;
}
SDL_AtomicDecRef(&m_QueuedFrames);
} }
+9 -5
View File
@@ -10,15 +10,16 @@ extern "C" {
class FFmpegVideoDecoder : public IVideoDecoder { class FFmpegVideoDecoder : public IVideoDecoder {
public: public:
FFmpegVideoDecoder(); FFmpegVideoDecoder(bool testOnly);
virtual ~FFmpegVideoDecoder(); virtual ~FFmpegVideoDecoder() override;
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds, virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, SDL_Window* window,
int videoFormat, int videoFormat,
int width, int width,
int height, int height,
int maxFps, int maxFps,
bool enableVsync) override; bool enableVsync,
bool enableFramePacing) override;
virtual bool isHardwareAccelerated() override; virtual bool isHardwareAccelerated() override;
virtual int getDecoderCapabilities() override; virtual int getDecoderCapabilities() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override; virtual int submitDecodeUnit(PDECODE_UNIT du) override;
@@ -30,7 +31,9 @@ public:
private: private:
bool completeInitialization(AVCodec* decoder, SDL_Window* window, bool completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height, int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly); int maxFps, bool enableFramePacing, bool testFrame);
void stringifyVideoStats(VIDEO_STATS& stats, char* output);
void logVideoStats(VIDEO_STATS& stats, const char* title); void logVideoStats(VIDEO_STATS& stats, const char* title);
@@ -51,15 +54,16 @@ private:
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; int m_ConsecutiveFailedDecodes;
Pacer* m_Pacer; Pacer* m_Pacer;
VIDEO_STATS m_ActiveWndVideoStats; VIDEO_STATS m_ActiveWndVideoStats;
VIDEO_STATS m_LastWndVideoStats; VIDEO_STATS m_LastWndVideoStats;
VIDEO_STATS m_GlobalVideoStats; VIDEO_STATS m_GlobalVideoStats;
int m_LastFrameNumber; int m_LastFrameNumber;
int m_StreamFps; int m_StreamFps;
bool m_NeedsSpsFixup; bool m_NeedsSpsFixup;
bool m_TestOnly;
static const uint8_t k_H264TestFrame[]; static const uint8_t k_H264TestFrame[];
static const uint8_t k_HEVCTestFrame[]; static const uint8_t k_HEVCTestFrame[];
+64
View File
@@ -0,0 +1,64 @@
#include "overlaymanager.h"
using namespace Overlay;
OverlayManager::OverlayManager() :
m_Renderer(nullptr)
{
memset(m_Overlays, 0, sizeof(m_Overlays));
m_Overlays[OverlayType::OverlayDebug].color = {0xD0, 0xD0, 0x00, 0xFF};
m_Overlays[OverlayType::OverlayDebug].fontSize = 20;
}
bool OverlayManager::isOverlayEnabled(OverlayType type)
{
return m_Overlays[type].enabled;
}
char* OverlayManager::getOverlayText(OverlayType type)
{
return m_Overlays[type].text;
}
int OverlayManager::getOverlayFontSize(OverlayType type)
{
return m_Overlays[type].fontSize;
}
void OverlayManager::setOverlayTextUpdated(OverlayType type)
{
// Only update the overlay state if it's enabled. If it's not enabled,
// the renderer has already been notified by setOverlayState().
if (m_Overlays[type].enabled && m_Renderer != nullptr) {
m_Renderer->notifyOverlayUpdated(type);
}
}
void OverlayManager::setOverlayState(OverlayType type, bool enabled)
{
bool stateChanged = m_Overlays[type].enabled != enabled;
m_Overlays[type].enabled = enabled;
if (stateChanged) {
if (!enabled) {
// Set the text to empty string on disable
m_Overlays[type].text[0] = 0;
}
if (m_Renderer != nullptr) {
m_Renderer->notifyOverlayUpdated(type);
}
}
}
SDL_Color OverlayManager::getOverlayColor(OverlayType type)
{
return m_Overlays[type].color;
}
void OverlayManager::setOverlayRenderer(IOverlayRenderer* renderer)
{
m_Renderer = renderer;
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <QString>
#include <SDL.h>
namespace Overlay {
enum OverlayType {
OverlayDebug,
OverlayMinorNotification,
OverlayMajorNotification,
OverlayMax
};
class IOverlayRenderer
{
public:
virtual ~IOverlayRenderer() = default;
virtual void notifyOverlayUpdated(OverlayType type) = 0;
};
class OverlayManager
{
public:
OverlayManager();
bool isOverlayEnabled(OverlayType type);
char* getOverlayText(OverlayType type);
void setOverlayTextUpdated(OverlayType type);
void setOverlayState(OverlayType type, bool enabled);
SDL_Color getOverlayColor(OverlayType type);
int getOverlayFontSize(OverlayType type);
void setOverlayRenderer(IOverlayRenderer* renderer);
struct {
bool enabled;
int fontSize;
SDL_Color color;
char text[512];
} m_Overlays[OverlayMax];
IOverlayRenderer* m_Renderer;
};
}
+2 -2
View File
@@ -1,6 +1,6 @@
#include "sl.h" #include "sl.h"
SLVideoDecoder::SLVideoDecoder() SLVideoDecoder::SLVideoDecoder(bool)
: m_VideoContext(nullptr), : m_VideoContext(nullptr),
m_VideoStream(nullptr) m_VideoStream(nullptr)
{ {
@@ -34,7 +34,7 @@ SLVideoDecoder::getDecoderCapabilities()
bool bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds, SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*, SDL_Window*,
int videoFormat, int, int, int frameRate, bool) int videoFormat, int, int, int frameRate, bool, bool)
{ {
// SLVideo only supports hardware decoding // SLVideo only supports hardware decoding
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) { if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
+3 -2
View File
@@ -7,7 +7,7 @@
class SLVideoDecoder : public IVideoDecoder class SLVideoDecoder : public IVideoDecoder
{ {
public: public:
SLVideoDecoder(); SLVideoDecoder(bool testOnly);
virtual ~SLVideoDecoder(); virtual ~SLVideoDecoder();
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds, virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, SDL_Window* window,
@@ -15,7 +15,8 @@ public:
int width, int width,
int height, int height,
int frameRate, int frameRate,
bool enableVsync); bool enableVsync,
bool enableFramePacing);
virtual bool isHardwareAccelerated(); virtual bool isHardwareAccelerated();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual int submitDecodeUnit(PDECODE_UNIT du); virtual int submitDecodeUnit(PDECODE_UNIT du);
+1
View File
@@ -0,0 +1 @@
0.9.1
+1
View File
@@ -0,0 +1 @@
Versions/Current/Frameworks
+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_ttf
@@ -0,0 +1 @@
Versions/Current/FreeType
@@ -0,0 +1 @@
Versions/Current/Headers
@@ -0,0 +1 @@
Versions/Current/Resources
@@ -0,0 +1,603 @@
/* ftconfig.h. Generated from ftconfig.in by configure. */
/***************************************************************************/
/* */
/* ftconfig.in */
/* */
/* UNIX-specific configuration file (specification only). */
/* */
/* Copyright 1996-2018 by */
/* David Turner, Robert Wilhelm, and Werner Lemberg. */
/* */
/* This file is part of the FreeType project, and may only be used, */
/* modified, and distributed under the terms of the FreeType project */
/* license, LICENSE.TXT. By continuing to use, modify, or distribute */
/* this file you indicate that you have read the license and */
/* understand and accept it fully. */
/* */
/***************************************************************************/
/*************************************************************************/
/* */
/* This header file contains a number of macro definitions that are used */
/* by the rest of the engine. Most of the macros here are automatically */
/* determined at compile time, and you should not need to change it to */
/* port FreeType, except to compile the library with a non-ANSI */
/* compiler. */
/* */
/* Note however that if some specific modifications are needed, we */
/* advise you to place a modified copy in your build directory. */
/* */
/* The build directory is usually `builds/<system>', and contains */
/* system-specific files that are always included first when building */
/* the library. */
/* */
/*************************************************************************/
#ifndef FTCONFIG_H_
#define FTCONFIG_H_
#include <ft2build.h>
#include FT_CONFIG_OPTIONS_H
#include FT_CONFIG_STANDARD_LIBRARY_H
FT_BEGIN_HEADER
/*************************************************************************/
/* */
/* PLATFORM-SPECIFIC CONFIGURATION MACROS */
/* */
/* These macros can be toggled to suit a specific system. The current */
/* ones are defaults used to compile FreeType in an ANSI C environment */
/* (16bit compilers are also supported). Copy this file to your own */
/* `builds/<system>' directory, and edit it to port the engine. */
/* */
/*************************************************************************/
#define HAVE_UNISTD_H 1
#define HAVE_FCNTL_H 1
#define HAVE_STDINT_H 1
/* There are systems (like the Texas Instruments 'C54x) where a `char' */
/* has 16 bits. ANSI C says that sizeof(char) is always 1. Since an */
/* `int' has 16 bits also for this system, sizeof(int) gives 1 which */
/* is probably unexpected. */
/* */
/* `CHAR_BIT' (defined in limits.h) gives the number of bits in a */
/* `char' type. */
#ifndef FT_CHAR_BIT
#define FT_CHAR_BIT CHAR_BIT
#endif
/* #undef FT_USE_AUTOCONF_SIZEOF_TYPES */
#ifdef FT_USE_AUTOCONF_SIZEOF_TYPES
#define SIZEOF_INT 4
#define SIZEOF_LONG 8
#define FT_SIZEOF_INT SIZEOF_INT
#define FT_SIZEOF_LONG SIZEOF_LONG
#else /* !FT_USE_AUTOCONF_SIZEOF_TYPES */
/* Following cpp computation of the bit length of int and long */
/* is copied from default include/freetype/config/ftconfig.h. */
/* If any improvement is required for this file, it should be */
/* applied to the original header file for the builders that */
/* do not use configure script. */
/* The size of an `int' type. */
#if FT_UINT_MAX == 0xFFFFUL
#define FT_SIZEOF_INT (16 / FT_CHAR_BIT)
#elif FT_UINT_MAX == 0xFFFFFFFFUL
#define FT_SIZEOF_INT (32 / FT_CHAR_BIT)
#elif FT_UINT_MAX > 0xFFFFFFFFUL && FT_UINT_MAX == 0xFFFFFFFFFFFFFFFFUL
#define FT_SIZEOF_INT (64 / FT_CHAR_BIT)
#else
#error "Unsupported size of `int' type!"
#endif
/* The size of a `long' type. A five-byte `long' (as used e.g. on the */
/* DM642) is recognized but avoided. */
#if FT_ULONG_MAX == 0xFFFFFFFFUL
#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT)
#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFUL
#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT)
#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFFFFFFFUL
#define FT_SIZEOF_LONG (64 / FT_CHAR_BIT)
#else
#error "Unsupported size of `long' type!"
#endif
#endif /* !FT_USE_AUTOCONF_SIZEOF_TYPES */
/* FT_UNUSED is a macro used to indicate that a given parameter is not */
/* used -- this is only used to get rid of unpleasant compiler warnings */
#ifndef FT_UNUSED
#define FT_UNUSED( arg ) ( (arg) = (arg) )
#endif
/*************************************************************************/
/* */
/* AUTOMATIC CONFIGURATION MACROS */
/* */
/* These macros are computed from the ones defined above. Don't touch */
/* their definition, unless you know precisely what you are doing. No */
/* porter should need to mess with them. */
/* */
/*************************************************************************/
/*************************************************************************/
/* */
/* Mac support */
/* */
/* This is the only necessary change, so it is defined here instead */
/* providing a new configuration file. */
/* */
#if defined( __APPLE__ ) || ( defined( __MWERKS__ ) && defined( macintosh ) )
/* no Carbon frameworks for 64bit 10.4.x */
/* AvailabilityMacros.h is available since Mac OS X 10.2, */
/* so guess the system version by maximum errno before inclusion */
#include <errno.h>
#ifdef ECANCELED /* defined since 10.2 */
#include "AvailabilityMacros.h"
#endif
#if defined( __LP64__ ) && \
( MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 )
#undef FT_MACINTOSH
#endif
#elif defined( __SC__ ) || defined( __MRC__ )
/* Classic MacOS compilers */
#include "ConditionalMacros.h"
#if TARGET_OS_MAC
#define FT_MACINTOSH 1
#endif
#endif
/* Fix compiler warning with sgi compiler */
#if defined( __sgi ) && !defined( __GNUC__ )
#if defined( _COMPILER_VERSION ) && ( _COMPILER_VERSION >= 730 )
#pragma set woff 3505
#endif
#endif
/*************************************************************************/
/* */
/* <Section> */
/* basic_types */
/* */
/*************************************************************************/
/*************************************************************************/
/* */
/* <Type> */
/* FT_Int16 */
/* */
/* <Description> */
/* A typedef for a 16bit signed integer type. */
/* */
typedef signed short FT_Int16;
/*************************************************************************/
/* */
/* <Type> */
/* FT_UInt16 */
/* */
/* <Description> */
/* A typedef for a 16bit unsigned integer type. */
/* */
typedef unsigned short FT_UInt16;
/* */
/* this #if 0 ... #endif clause is for documentation purposes */
#if 0
/*************************************************************************/
/* */
/* <Type> */
/* FT_Int32 */
/* */
/* <Description> */
/* A typedef for a 32bit signed integer type. The size depends on */
/* the configuration. */
/* */
typedef signed XXX FT_Int32;
/*************************************************************************/
/* */
/* <Type> */
/* FT_UInt32 */
/* */
/* A typedef for a 32bit unsigned integer type. The size depends on */
/* the configuration. */
/* */
typedef unsigned XXX FT_UInt32;
/*************************************************************************/
/* */
/* <Type> */
/* FT_Int64 */
/* */
/* A typedef for a 64bit signed integer type. The size depends on */
/* the configuration. Only defined if there is real 64bit support; */
/* otherwise, it gets emulated with a structure (if necessary). */
/* */
typedef signed XXX FT_Int64;
/*************************************************************************/
/* */
/* <Type> */
/* FT_UInt64 */
/* */
/* A typedef for a 64bit unsigned integer type. The size depends on */
/* the configuration. Only defined if there is real 64bit support; */
/* otherwise, it gets emulated with a structure (if necessary). */
/* */
typedef unsigned XXX FT_UInt64;
/* */
#endif
#if FT_SIZEOF_INT == 4
typedef signed int FT_Int32;
typedef unsigned int FT_UInt32;
#elif FT_SIZEOF_LONG == 4
typedef signed long FT_Int32;
typedef unsigned long FT_UInt32;
#else
#error "no 32bit type found -- please check your configuration files"
#endif
/* look up an integer type that is at least 32 bits */
#if FT_SIZEOF_INT >= 4
typedef int FT_Fast;
typedef unsigned int FT_UFast;
#elif FT_SIZEOF_LONG >= 4
typedef long FT_Fast;
typedef unsigned long FT_UFast;
#endif
/* determine whether we have a 64-bit int type */
/* (mostly for environments without `autoconf') */
#if FT_SIZEOF_LONG == 8
/* FT_LONG64 must be defined if a 64-bit type is available */
#define FT_LONG64
#define FT_INT64 long
#define FT_UINT64 unsigned long
/* we handle the LLP64 scheme separately for GCC and clang, */
/* suppressing the `long long' warning */
#elif ( FT_SIZEOF_LONG == 4 ) && \
defined( HAVE_LONG_LONG_INT ) && \
defined( __GNUC__ )
#pragma GCC diagnostic ignored "-Wlong-long"
#define FT_LONG64
#define FT_INT64 long long int
#define FT_UINT64 unsigned long long int
/*************************************************************************/
/* */
/* A 64-bit data type may create compilation problems if you compile */
/* in strict ANSI mode. To avoid them, we disable other 64-bit data */
/* types if __STDC__ is defined. You can however ignore this rule */
/* by defining the FT_CONFIG_OPTION_FORCE_INT64 configuration macro. */
/* */
#elif !defined( __STDC__ ) || defined( FT_CONFIG_OPTION_FORCE_INT64 )
#if defined( __STDC_VERSION__ ) && __STDC_VERSION__ >= 199901L
#define FT_LONG64
#define FT_INT64 long long int
#define FT_UINT64 unsigned long long int
#elif defined( _MSC_VER ) && _MSC_VER >= 900 /* Visual C++ (and Intel C++) */
/* this compiler provides the __int64 type */
#define FT_LONG64
#define FT_INT64 __int64
#define FT_UINT64 unsigned __int64
#elif defined( __BORLANDC__ ) /* Borland C++ */
/* XXXX: We should probably check the value of __BORLANDC__ in order */
/* to test the compiler version. */
/* this compiler provides the __int64 type */
#define FT_LONG64
#define FT_INT64 __int64
#define FT_UINT64 unsigned __int64
#elif defined( __WATCOMC__ ) /* Watcom C++ */
/* Watcom doesn't provide 64-bit data types */
#elif defined( __MWERKS__ ) /* Metrowerks CodeWarrior */
#define FT_LONG64
#define FT_INT64 long long int
#define FT_UINT64 unsigned long long int
#elif defined( __GNUC__ )
/* GCC provides the `long long' type */
#define FT_LONG64
#define FT_INT64 long long int
#define FT_UINT64 unsigned long long int
#endif /* __STDC_VERSION__ >= 199901L */
#endif /* FT_SIZEOF_LONG == 8 */
#ifdef FT_LONG64
typedef FT_INT64 FT_Int64;
typedef FT_UINT64 FT_UInt64;
#endif
#ifdef _WIN64
/* only 64bit Windows uses the LLP64 data model, i.e., */
/* 32bit integers, 64bit pointers */
#define FT_UINT_TO_POINTER( x ) (void*)(unsigned __int64)(x)
#else
#define FT_UINT_TO_POINTER( x ) (void*)(unsigned long)(x)
#endif
/*************************************************************************/
/* */
/* miscellaneous */
/* */
/*************************************************************************/
#define FT_BEGIN_STMNT do {
#define FT_END_STMNT } while ( 0 )
#define FT_DUMMY_STMNT FT_BEGIN_STMNT FT_END_STMNT
/* typeof condition taken from gnulib's `intprops.h' header file */
#if ( ( defined( __GNUC__ ) && __GNUC__ >= 2 ) || \
( defined( __IBMC__ ) && __IBMC__ >= 1210 && \
defined( __IBM__TYPEOF__ ) ) || \
( defined( __SUNPRO_C ) && __SUNPRO_C >= 0x5110 && !__STDC__ ) )
#define FT_TYPEOF( type ) ( __typeof__ ( type ) )
#else
#define FT_TYPEOF( type ) /* empty */
#endif
/* Use FT_LOCAL and FT_LOCAL_DEF to declare and define, respectively, */
/* a function that gets used only within the scope of a module. */
/* Normally, both the header and source code files for such a */
/* function are within a single module directory. */
/* */
/* Intra-module arrays should be tagged with FT_LOCAL_ARRAY and */
/* FT_LOCAL_ARRAY_DEF. */
/* */
#ifdef FT_MAKE_OPTION_SINGLE_OBJECT
#define FT_LOCAL( x ) static x
#define FT_LOCAL_DEF( x ) static x
#else
#ifdef __cplusplus
#define FT_LOCAL( x ) extern "C" x
#define FT_LOCAL_DEF( x ) extern "C" x
#else
#define FT_LOCAL( x ) extern x
#define FT_LOCAL_DEF( x ) x
#endif
#endif /* FT_MAKE_OPTION_SINGLE_OBJECT */
#define FT_LOCAL_ARRAY( x ) extern const x
#define FT_LOCAL_ARRAY_DEF( x ) const x
/* Use FT_BASE and FT_BASE_DEF to declare and define, respectively, */
/* functions that are used in more than a single module. In the */
/* current setup this implies that the declaration is in a header */
/* file in the `include/freetype/internal' directory, and the */
/* function body is in a file in `src/base'. */
/* */
#ifndef FT_BASE
#ifdef __cplusplus
#define FT_BASE( x ) extern "C" x
#else
#define FT_BASE( x ) extern x
#endif
#endif /* !FT_BASE */
#ifndef FT_BASE_DEF
#ifdef __cplusplus
#define FT_BASE_DEF( x ) x
#else
#define FT_BASE_DEF( x ) x
#endif
#endif /* !FT_BASE_DEF */
/* When compiling FreeType as a DLL or DSO with hidden visibility */
/* some systems/compilers need a special attribute in front OR after */
/* the return type of function declarations. */
/* */
/* Two macros are used within the FreeType source code to define */
/* exported library functions: FT_EXPORT and FT_EXPORT_DEF. */
/* */
/* FT_EXPORT( return_type ) */
/* */
/* is used in a function declaration, as in */
/* */
/* FT_EXPORT( FT_Error ) */
/* FT_Init_FreeType( FT_Library* alibrary ); */
/* */
/* */
/* FT_EXPORT_DEF( return_type ) */
/* */
/* is used in a function definition, as in */
/* */
/* FT_EXPORT_DEF( FT_Error ) */
/* FT_Init_FreeType( FT_Library* alibrary ) */
/* { */
/* ... some code ... */
/* return FT_Err_Ok; */
/* } */
/* */
/* You can provide your own implementation of FT_EXPORT and */
/* FT_EXPORT_DEF here if you want. */
/* */
/* To export a variable, use FT_EXPORT_VAR. */
/* */
#ifndef FT_EXPORT
#ifdef FT2_BUILD_LIBRARY
#if defined( _WIN32 ) && ( defined( _DLL ) || defined( DLL_EXPORT ) )
#define FT_EXPORT( x ) __declspec( dllexport ) x
#elif defined( __GNUC__ ) && __GNUC__ >= 4
#define FT_EXPORT( x ) __attribute__(( visibility( "default" ) )) x
#elif defined( __cplusplus )
#define FT_EXPORT( x ) extern "C" x
#else
#define FT_EXPORT( x ) extern x
#endif
#else
#if defined( FT2_DLLIMPORT )
#define FT_EXPORT( x ) __declspec( dllimport ) x
#elif defined( __cplusplus )
#define FT_EXPORT( x ) extern "C" x
#else
#define FT_EXPORT( x ) extern x
#endif
#endif
#endif /* !FT_EXPORT */
#ifndef FT_EXPORT_DEF
#ifdef __cplusplus
#define FT_EXPORT_DEF( x ) extern "C" x
#else
#define FT_EXPORT_DEF( x ) extern x
#endif
#endif /* !FT_EXPORT_DEF */
#ifndef FT_EXPORT_VAR
#ifdef __cplusplus
#define FT_EXPORT_VAR( x ) extern "C" x
#else
#define FT_EXPORT_VAR( x ) extern x
#endif
#endif /* !FT_EXPORT_VAR */
/* The following macros are needed to compile the library with a */
/* C++ compiler and with 16bit compilers. */
/* */
/* This is special. Within C++, you must specify `extern "C"' for */
/* functions which are used via function pointers, and you also */
/* must do that for structures which contain function pointers to */
/* assure C linkage -- it's not possible to have (local) anonymous */
/* functions which are accessed by (global) function pointers. */
/* */
/* */
/* FT_CALLBACK_DEF is used to _define_ a callback function, */
/* located in the same source code file as the structure that uses */
/* it. */
/* */
/* FT_BASE_CALLBACK and FT_BASE_CALLBACK_DEF are used to declare */
/* and define a callback function, respectively, in a similar way */
/* as FT_BASE and FT_BASE_DEF work. */
/* */
/* FT_CALLBACK_TABLE is used to _declare_ a constant variable that */
/* contains pointers to callback functions. */
/* */
/* FT_CALLBACK_TABLE_DEF is used to _define_ a constant variable */
/* that contains pointers to callback functions. */
/* */
/* */
/* Some 16bit compilers have to redefine these macros to insert */
/* the infamous `_cdecl' or `__fastcall' declarations. */
/* */
#ifndef FT_CALLBACK_DEF
#ifdef __cplusplus
#define FT_CALLBACK_DEF( x ) extern "C" x
#else
#define FT_CALLBACK_DEF( x ) static x
#endif
#endif /* FT_CALLBACK_DEF */
#ifndef FT_BASE_CALLBACK
#ifdef __cplusplus
#define FT_BASE_CALLBACK( x ) extern "C" x
#define FT_BASE_CALLBACK_DEF( x ) extern "C" x
#else
#define FT_BASE_CALLBACK( x ) extern x
#define FT_BASE_CALLBACK_DEF( x ) x
#endif
#endif /* FT_BASE_CALLBACK */
#ifndef FT_CALLBACK_TABLE
#ifdef __cplusplus
#define FT_CALLBACK_TABLE extern "C"
#define FT_CALLBACK_TABLE_DEF extern "C"
#else
#define FT_CALLBACK_TABLE extern
#define FT_CALLBACK_TABLE_DEF /* nothing */
#endif
#endif /* FT_CALLBACK_TABLE */
FT_END_HEADER
#endif /* FTCONFIG_H_ */
/* END */
@@ -0,0 +1,804 @@
/***************************************************************************/
/* */
/* ftheader.h */
/* */
/* Build macros of the FreeType 2 library. */
/* */
/* Copyright 1996-2018 by */
/* David Turner, Robert Wilhelm, and Werner Lemberg. */
/* */
/* This file is part of the FreeType project, and may only be used, */
/* modified, and distributed under the terms of the FreeType project */
/* license, LICENSE.TXT. By continuing to use, modify, or distribute */
/* this file you indicate that you have read the license and */
/* understand and accept it fully. */
/* */
/***************************************************************************/
#ifndef FTHEADER_H_
#define FTHEADER_H_
/*@***********************************************************************/
/* */
/* <Macro> */
/* FT_BEGIN_HEADER */
/* */
/* <Description> */
/* This macro is used in association with @FT_END_HEADER in header */
/* files to ensure that the declarations within are properly */
/* encapsulated in an `extern "C" { .. }' block when included from a */
/* C++ compiler. */
/* */
#ifdef __cplusplus
#define FT_BEGIN_HEADER extern "C" {
#else
#define FT_BEGIN_HEADER /* nothing */
#endif
/*@***********************************************************************/
/* */
/* <Macro> */
/* FT_END_HEADER */
/* */
/* <Description> */
/* This macro is used in association with @FT_BEGIN_HEADER in header */
/* files to ensure that the declarations within are properly */
/* encapsulated in an `extern "C" { .. }' block when included from a */
/* C++ compiler. */
/* */
#ifdef __cplusplus
#define FT_END_HEADER }
#else
#define FT_END_HEADER /* nothing */
#endif
/*************************************************************************/
/* */
/* Aliases for the FreeType 2 public and configuration files. */
/* */
/*************************************************************************/
/*************************************************************************/
/* */
/* <Section> */
/* header_file_macros */
/* */
/* <Title> */
/* Header File Macros */
/* */
/* <Abstract> */
/* Macro definitions used to #include specific header files. */
/* */
/* <Description> */
/* The following macros are defined to the name of specific */
/* FreeType~2 header files. They can be used directly in #include */
/* statements as in: */
/* */
/* { */
/* #include FT_FREETYPE_H */
/* #include FT_MULTIPLE_MASTERS_H */
/* #include FT_GLYPH_H */
/* } */
/* */
/* There are several reasons why we are now using macros to name */
/* public header files. The first one is that such macros are not */
/* limited to the infamous 8.3~naming rule required by DOS (and */
/* `FT_MULTIPLE_MASTERS_H' is a lot more meaningful than `ftmm.h'). */
/* */
/* The second reason is that it allows for more flexibility in the */
/* way FreeType~2 is installed on a given system. */
/* */
/*************************************************************************/
/* configuration files */
/*************************************************************************
*
* @macro:
* FT_CONFIG_CONFIG_H
*
* @description:
* A macro used in #include statements to name the file containing
* FreeType~2 configuration data.
*
*/
#ifndef FT_CONFIG_CONFIG_H
#define FT_CONFIG_CONFIG_H <freetype/config/ftconfig.h>
#endif
/*************************************************************************
*
* @macro:
* FT_CONFIG_STANDARD_LIBRARY_H
*
* @description:
* A macro used in #include statements to name the file containing
* FreeType~2 interface to the standard C library functions.
*
*/
#ifndef FT_CONFIG_STANDARD_LIBRARY_H
#define FT_CONFIG_STANDARD_LIBRARY_H <freetype/config/ftstdlib.h>
#endif
/*************************************************************************
*
* @macro:
* FT_CONFIG_OPTIONS_H
*
* @description:
* A macro used in #include statements to name the file containing
* FreeType~2 project-specific configuration options.
*
*/
#ifndef FT_CONFIG_OPTIONS_H
#define FT_CONFIG_OPTIONS_H <freetype/config/ftoption.h>
#endif
/*************************************************************************
*
* @macro:
* FT_CONFIG_MODULES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* list of FreeType~2 modules that are statically linked to new library
* instances in @FT_Init_FreeType.
*
*/
#ifndef FT_CONFIG_MODULES_H
#define FT_CONFIG_MODULES_H <freetype/config/ftmodule.h>
#endif
/* */
/* public headers */
/*************************************************************************
*
* @macro:
* FT_FREETYPE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* base FreeType~2 API.
*
*/
#define FT_FREETYPE_H <freetype/freetype.h>
/*************************************************************************
*
* @macro:
* FT_ERRORS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* list of FreeType~2 error codes (and messages).
*
* It is included by @FT_FREETYPE_H.
*
*/
#define FT_ERRORS_H <freetype/fterrors.h>
/*************************************************************************
*
* @macro:
* FT_MODULE_ERRORS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* list of FreeType~2 module error offsets (and messages).
*
*/
#define FT_MODULE_ERRORS_H <freetype/ftmoderr.h>
/*************************************************************************
*
* @macro:
* FT_SYSTEM_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 interface to low-level operations (i.e., memory management
* and stream i/o).
*
* It is included by @FT_FREETYPE_H.
*
*/
#define FT_SYSTEM_H <freetype/ftsystem.h>
/*************************************************************************
*
* @macro:
* FT_IMAGE_H
*
* @description:
* A macro used in #include statements to name the file containing type
* definitions related to glyph images (i.e., bitmaps, outlines,
* scan-converter parameters).
*
* It is included by @FT_FREETYPE_H.
*
*/
#define FT_IMAGE_H <freetype/ftimage.h>
/*************************************************************************
*
* @macro:
* FT_TYPES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* basic data types defined by FreeType~2.
*
* It is included by @FT_FREETYPE_H.
*
*/
#define FT_TYPES_H <freetype/fttypes.h>
/*************************************************************************
*
* @macro:
* FT_LIST_H
*
* @description:
* A macro used in #include statements to name the file containing the
* list management API of FreeType~2.
*
* (Most applications will never need to include this file.)
*
*/
#define FT_LIST_H <freetype/ftlist.h>
/*************************************************************************
*
* @macro:
* FT_OUTLINE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* scalable outline management API of FreeType~2.
*
*/
#define FT_OUTLINE_H <freetype/ftoutln.h>
/*************************************************************************
*
* @macro:
* FT_SIZES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API which manages multiple @FT_Size objects per face.
*
*/
#define FT_SIZES_H <freetype/ftsizes.h>
/*************************************************************************
*
* @macro:
* FT_MODULE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* module management API of FreeType~2.
*
*/
#define FT_MODULE_H <freetype/ftmodapi.h>
/*************************************************************************
*
* @macro:
* FT_RENDER_H
*
* @description:
* A macro used in #include statements to name the file containing the
* renderer module management API of FreeType~2.
*
*/
#define FT_RENDER_H <freetype/ftrender.h>
/*************************************************************************
*
* @macro:
* FT_DRIVER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the driver modules.
*
*/
#define FT_DRIVER_H <freetype/ftdriver.h>
/*************************************************************************
*
* @macro:
* FT_AUTOHINTER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the auto-hinting module.
*
* Deprecated since version 2.9; use @FT_DRIVER_H instead.
*
*/
#define FT_AUTOHINTER_H FT_DRIVER_H
/*************************************************************************
*
* @macro:
* FT_CFF_DRIVER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the CFF driver module.
*
* Deprecated since version 2.9; use @FT_DRIVER_H instead.
*
*/
#define FT_CFF_DRIVER_H FT_DRIVER_H
/*************************************************************************
*
* @macro:
* FT_TRUETYPE_DRIVER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the TrueType driver module.
*
* Deprecated since version 2.9; use @FT_DRIVER_H instead.
*
*/
#define FT_TRUETYPE_DRIVER_H FT_DRIVER_H
/*************************************************************************
*
* @macro:
* FT_PCF_DRIVER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the PCF driver module.
*
* Deprecated since version 2.9; use @FT_DRIVER_H instead.
*
*/
#define FT_PCF_DRIVER_H FT_DRIVER_H
/*************************************************************************
*
* @macro:
* FT_TYPE1_TABLES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* types and API specific to the Type~1 format.
*
*/
#define FT_TYPE1_TABLES_H <freetype/t1tables.h>
/*************************************************************************
*
* @macro:
* FT_TRUETYPE_IDS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* enumeration values which identify name strings, languages, encodings,
* etc. This file really contains a _large_ set of constant macro
* definitions, taken from the TrueType and OpenType specifications.
*
*/
#define FT_TRUETYPE_IDS_H <freetype/ttnameid.h>
/*************************************************************************
*
* @macro:
* FT_TRUETYPE_TABLES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* types and API specific to the TrueType (as well as OpenType) format.
*
*/
#define FT_TRUETYPE_TABLES_H <freetype/tttables.h>
/*************************************************************************
*
* @macro:
* FT_TRUETYPE_TAGS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of TrueType four-byte `tags' which identify blocks in
* SFNT-based font formats (i.e., TrueType and OpenType).
*
*/
#define FT_TRUETYPE_TAGS_H <freetype/tttags.h>
/*************************************************************************
*
* @macro:
* FT_BDF_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which accesses BDF-specific strings from a
* face.
*
*/
#define FT_BDF_H <freetype/ftbdf.h>
/*************************************************************************
*
* @macro:
* FT_CID_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which access CID font information from a
* face.
*
*/
#define FT_CID_H <freetype/ftcid.h>
/*************************************************************************
*
* @macro:
* FT_GZIP_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which supports gzip-compressed files.
*
*/
#define FT_GZIP_H <freetype/ftgzip.h>
/*************************************************************************
*
* @macro:
* FT_LZW_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which supports LZW-compressed files.
*
*/
#define FT_LZW_H <freetype/ftlzw.h>
/*************************************************************************
*
* @macro:
* FT_BZIP2_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which supports bzip2-compressed files.
*
*/
#define FT_BZIP2_H <freetype/ftbzip2.h>
/*************************************************************************
*
* @macro:
* FT_WINFONTS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which supports Windows FNT files.
*
*/
#define FT_WINFONTS_H <freetype/ftwinfnt.h>
/*************************************************************************
*
* @macro:
* FT_GLYPH_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API of the optional glyph management component.
*
*/
#define FT_GLYPH_H <freetype/ftglyph.h>
/*************************************************************************
*
* @macro:
* FT_BITMAP_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API of the optional bitmap conversion component.
*
*/
#define FT_BITMAP_H <freetype/ftbitmap.h>
/*************************************************************************
*
* @macro:
* FT_BBOX_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API of the optional exact bounding box computation routines.
*
*/
#define FT_BBOX_H <freetype/ftbbox.h>
/*************************************************************************
*
* @macro:
* FT_CACHE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API of the optional FreeType~2 cache sub-system.
*
*/
#define FT_CACHE_H <freetype/ftcache.h>
/*************************************************************************
*
* @macro:
* FT_MAC_H
*
* @description:
* A macro used in #include statements to name the file containing the
* Macintosh-specific FreeType~2 API. The latter is used to access
* fonts embedded in resource forks.
*
* This header file must be explicitly included by client applications
* compiled on the Mac (note that the base API still works though).
*
*/
#define FT_MAC_H <freetype/ftmac.h>
/*************************************************************************
*
* @macro:
* FT_MULTIPLE_MASTERS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* optional multiple-masters management API of FreeType~2.
*
*/
#define FT_MULTIPLE_MASTERS_H <freetype/ftmm.h>
/*************************************************************************
*
* @macro:
* FT_SFNT_NAMES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* optional FreeType~2 API which accesses embedded `name' strings in
* SFNT-based font formats (i.e., TrueType and OpenType).
*
*/
#define FT_SFNT_NAMES_H <freetype/ftsnames.h>
/*************************************************************************
*
* @macro:
* FT_OPENTYPE_VALIDATE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* optional FreeType~2 API which validates OpenType tables (BASE, GDEF,
* GPOS, GSUB, JSTF).
*
*/
#define FT_OPENTYPE_VALIDATE_H <freetype/ftotval.h>
/*************************************************************************
*
* @macro:
* FT_GX_VALIDATE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* optional FreeType~2 API which validates TrueTypeGX/AAT tables (feat,
* mort, morx, bsln, just, kern, opbd, trak, prop).
*
*/
#define FT_GX_VALIDATE_H <freetype/ftgxval.h>
/*************************************************************************
*
* @macro:
* FT_PFR_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which accesses PFR-specific data.
*
*/
#define FT_PFR_H <freetype/ftpfr.h>
/*************************************************************************
*
* @macro:
* FT_STROKER_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which provides functions to stroke outline paths.
*/
#define FT_STROKER_H <freetype/ftstroke.h>
/*************************************************************************
*
* @macro:
* FT_SYNTHESIS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which performs artificial obliquing and emboldening.
*/
#define FT_SYNTHESIS_H <freetype/ftsynth.h>
/*************************************************************************
*
* @macro:
* FT_FONT_FORMATS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which provides functions specific to font formats.
*/
#define FT_FONT_FORMATS_H <freetype/ftfntfmt.h>
/* deprecated */
#define FT_XFREE86_H FT_FONT_FORMATS_H
/*************************************************************************
*
* @macro:
* FT_TRIGONOMETRY_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which performs trigonometric computations (e.g.,
* cosines and arc tangents).
*/
#define FT_TRIGONOMETRY_H <freetype/fttrigon.h>
/*************************************************************************
*
* @macro:
* FT_LCD_FILTER_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which performs color filtering for subpixel rendering.
*/
#define FT_LCD_FILTER_H <freetype/ftlcdfil.h>
/*************************************************************************
*
* @macro:
* FT_INCREMENTAL_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which performs incremental glyph loading.
*/
#define FT_INCREMENTAL_H <freetype/ftincrem.h>
/*************************************************************************
*
* @macro:
* FT_GASP_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which returns entries from the TrueType GASP table.
*/
#define FT_GASP_H <freetype/ftgasp.h>
/*************************************************************************
*
* @macro:
* FT_ADVANCES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which returns individual and ranged glyph advances.
*/
#define FT_ADVANCES_H <freetype/ftadvanc.h>
/* */
/* These header files don't need to be included by the user. */
#define FT_ERROR_DEFINITIONS_H <freetype/fterrdef.h>
#define FT_PARAMETER_TAGS_H <freetype/ftparams.h>
/* Deprecated macros. */
#define FT_UNPATENTED_HINTING_H <freetype/ftparams.h>
#define FT_TRUETYPE_UNPATENTED_H <freetype/ftparams.h>
/* FT_CACHE_H is the only header file needed for the cache subsystem. */
#define FT_CACHE_IMAGE_H FT_CACHE_H
#define FT_CACHE_SMALL_BITMAPS_H FT_CACHE_H
#define FT_CACHE_CHARMAP_H FT_CACHE_H
/* The internals of the cache sub-system are no longer exposed. We */
/* default to FT_CACHE_H at the moment just in case, but we know of */
/* no rogue client that uses them. */
/* */
#define FT_CACHE_MANAGER_H FT_CACHE_H
#define FT_CACHE_INTERNAL_MRU_H FT_CACHE_H
#define FT_CACHE_INTERNAL_MANAGER_H FT_CACHE_H
#define FT_CACHE_INTERNAL_CACHE_H FT_CACHE_H
#define FT_CACHE_INTERNAL_GLYPH_H FT_CACHE_H
#define FT_CACHE_INTERNAL_IMAGE_H FT_CACHE_H
#define FT_CACHE_INTERNAL_SBITS_H FT_CACHE_H
/*
* Include internal headers definitions from <internal/...>
* only when building the library.
*/
#ifdef FT2_BUILD_LIBRARY
#define FT_INTERNAL_INTERNAL_H <freetype/internal/internal.h>
#include FT_INTERNAL_INTERNAL_H
#endif /* FT2_BUILD_LIBRARY */
#endif /* FTHEADER_H_ */
/* END */
@@ -0,0 +1,20 @@
/* This is a generated file. */
FT_USE_MODULE( FT_Driver_ClassRec, tt_driver_class )
FT_USE_MODULE( FT_Driver_ClassRec, t1_driver_class )
FT_USE_MODULE( FT_Driver_ClassRec, cff_driver_class )
FT_USE_MODULE( FT_Driver_ClassRec, t1cid_driver_class )
FT_USE_MODULE( FT_Driver_ClassRec, pfr_driver_class )
FT_USE_MODULE( FT_Driver_ClassRec, t42_driver_class )
FT_USE_MODULE( FT_Driver_ClassRec, winfnt_driver_class )
FT_USE_MODULE( FT_Driver_ClassRec, pcf_driver_class )
FT_USE_MODULE( FT_Driver_ClassRec, bdf_driver_class )
FT_USE_MODULE( FT_Module_Class, sfnt_module_class )
FT_USE_MODULE( FT_Module_Class, autofit_module_class )
FT_USE_MODULE( FT_Module_Class, pshinter_module_class )
FT_USE_MODULE( FT_Renderer_Class, ft_raster1_renderer_class )
FT_USE_MODULE( FT_Renderer_Class, ft_smooth_renderer_class )
FT_USE_MODULE( FT_Renderer_Class, ft_smooth_lcd_renderer_class )
FT_USE_MODULE( FT_Renderer_Class, ft_smooth_lcdv_renderer_class )
FT_USE_MODULE( FT_Module_Class, psaux_module_class )
FT_USE_MODULE( FT_Module_Class, psnames_module_class )
/* EOF */
@@ -0,0 +1,977 @@
/***************************************************************************/
/* */
/* ftoption.h */
/* */
/* User-selectable configuration macros (specification only). */
/* */
/* Copyright 1996-2018 by */
/* David Turner, Robert Wilhelm, and Werner Lemberg. */
/* */
/* This file is part of the FreeType project, and may only be used, */
/* modified, and distributed under the terms of the FreeType project */
/* license, LICENSE.TXT. By continuing to use, modify, or distribute */
/* this file you indicate that you have read the license and */
/* understand and accept it fully. */
/* */
/***************************************************************************/
#ifndef FTOPTION_H_
#define FTOPTION_H_
#include <ft2build.h>
FT_BEGIN_HEADER
/*************************************************************************/
/* */
/* USER-SELECTABLE CONFIGURATION MACROS */
/* */
/* This file contains the default configuration macro definitions for */
/* a standard build of the FreeType library. There are three ways to */
/* use this file to build project-specific versions of the library: */
/* */
/* - You can modify this file by hand, but this is not recommended in */
/* cases where you would like to build several versions of the */
/* library from a single source directory. */
/* */
/* - You can put a copy of this file in your build directory, more */
/* precisely in `$BUILD/freetype/config/ftoption.h', where `$BUILD' */
/* is the name of a directory that is included _before_ the FreeType */
/* include path during compilation. */
/* */
/* The default FreeType Makefiles and Jamfiles use the build */
/* directory `builds/<system>' by default, but you can easily change */
/* that for your own projects. */
/* */
/* - Copy the file <ft2build.h> to `$BUILD/ft2build.h' and modify it */
/* slightly to pre-define the macro FT_CONFIG_OPTIONS_H used to */
/* locate this file during the build. For example, */
/* */
/* #define FT_CONFIG_OPTIONS_H <myftoptions.h> */
/* #include <freetype/config/ftheader.h> */
/* */
/* will use `$BUILD/myftoptions.h' instead of this file for macro */
/* definitions. */
/* */
/* Note also that you can similarly pre-define the macro */
/* FT_CONFIG_MODULES_H used to locate the file listing of the modules */
/* that are statically linked to the library at compile time. By */
/* default, this file is <freetype/config/ftmodule.h>. */
/* */
/* We highly recommend using the third method whenever possible. */
/* */
/*************************************************************************/
/*************************************************************************/
/*************************************************************************/
/**** ****/
/**** G E N E R A L F R E E T Y P E 2 C O N F I G U R A T I O N ****/
/**** ****/
/*************************************************************************/
/*************************************************************************/
/*#***********************************************************************/
/* */
/* If you enable this configuration option, FreeType recognizes an */
/* environment variable called `FREETYPE_PROPERTIES', which can be used */
/* to control the various font drivers and modules. The controllable */
/* properties are listed in the section @properties. */
/* */
/* You have to undefine this configuration option on platforms that lack */
/* the concept of environment variables (and thus don't have the */
/* `getenv' function), for example Windows CE. */
/* */
/* `FREETYPE_PROPERTIES' has the following syntax form (broken here into */
/* multiple lines for better readability). */
/* */
/* { */
/* <optional whitespace> */
/* <module-name1> ':' */
/* <property-name1> '=' <property-value1> */
/* <whitespace> */
/* <module-name2> ':' */
/* <property-name2> '=' <property-value2> */
/* ... */
/* } */
/* */
/* Example: */
/* */
/* FREETYPE_PROPERTIES=truetype:interpreter-version=35 \ */
/* cff:no-stem-darkening=1 \ */
/* autofitter:warping=1 */
/* */
#define FT_CONFIG_OPTION_ENVIRONMENT_PROPERTIES
/*************************************************************************/
/* */
/* Uncomment the line below if you want to activate LCD rendering */
/* technology similar to ClearType in this build of the library. This */
/* technology triples the resolution in the direction color subpixels. */
/* To mitigate color fringes inherent to this technology, you also need */
/* to explicitly set up LCD filtering. */
/* */
/* Note that this feature is covered by several Microsoft patents */
/* and should not be activated in any default build of the library. */
/* When this macro is not defined, FreeType offers alternative LCD */
/* rendering technology that produces excellent output without LCD */
/* filtering. */
/* */
/* #define FT_CONFIG_OPTION_SUBPIXEL_RENDERING */
/*************************************************************************/
/* */
/* Many compilers provide a non-ANSI 64-bit data type that can be used */
/* by FreeType to speed up some computations. However, this will create */
/* some problems when compiling the library in strict ANSI mode. */
/* */
/* For this reason, the use of 64-bit integers is normally disabled when */
/* the __STDC__ macro is defined. You can however disable this by */
/* defining the macro FT_CONFIG_OPTION_FORCE_INT64 here. */
/* */
/* For most compilers, this will only create compilation warnings when */
/* building the library. */
/* */
/* ObNote: The compiler-specific 64-bit integers are detected in the */
/* file `ftconfig.h' either statically or through the */
/* `configure' script on supported platforms. */
/* */
#undef FT_CONFIG_OPTION_FORCE_INT64
/*************************************************************************/
/* */
/* If this macro is defined, do not try to use an assembler version of */
/* performance-critical functions (e.g. FT_MulFix). You should only do */
/* that to verify that the assembler function works properly, or to */
/* execute benchmark tests of the various implementations. */
/* #define FT_CONFIG_OPTION_NO_ASSEMBLER */
/*************************************************************************/
/* */
/* If this macro is defined, try to use an inlined assembler version of */
/* the `FT_MulFix' function, which is a `hotspot' when loading and */
/* hinting glyphs, and which should be executed as fast as possible. */
/* */
/* Note that if your compiler or CPU is not supported, this will default */
/* to the standard and portable implementation found in `ftcalc.c'. */
/* */
#define FT_CONFIG_OPTION_INLINE_MULFIX
/*************************************************************************/
/* */
/* LZW-compressed file support. */
/* */
/* FreeType now handles font files that have been compressed with the */
/* `compress' program. This is mostly used to parse many of the PCF */
/* files that come with various X11 distributions. The implementation */
/* uses NetBSD's `zopen' to partially uncompress the file on the fly */
/* (see src/lzw/ftgzip.c). */
/* */
/* Define this macro if you want to enable this `feature'. */
/* */
#define FT_CONFIG_OPTION_USE_LZW
/*************************************************************************/
/* */
/* Gzip-compressed file support. */
/* */
/* FreeType now handles font files that have been compressed with the */
/* `gzip' program. This is mostly used to parse many of the PCF files */
/* that come with XFree86. The implementation uses `zlib' to */
/* partially uncompress the file on the fly (see src/gzip/ftgzip.c). */
/* */
/* Define this macro if you want to enable this `feature'. See also */
/* the macro FT_CONFIG_OPTION_SYSTEM_ZLIB below. */
/* */
#define FT_CONFIG_OPTION_USE_ZLIB
/*************************************************************************/
/* */
/* ZLib library selection */
/* */
/* This macro is only used when FT_CONFIG_OPTION_USE_ZLIB is defined. */
/* It allows FreeType's `ftgzip' component to link to the system's */
/* installation of the ZLib library. This is useful on systems like */
/* Unix or VMS where it generally is already available. */
/* */
/* If you let it undefined, the component will use its own copy */
/* of the zlib sources instead. These have been modified to be */
/* included directly within the component and *not* export external */
/* function names. This allows you to link any program with FreeType */
/* _and_ ZLib without linking conflicts. */
/* */
/* Do not #undef this macro here since the build system might define */
/* it for certain configurations only. */
/* */
/* If you use a build system like cmake or the `configure' script, */
/* options set by those programs have precendence, overwriting the */
/* value here with the configured one. */
/* */
#define FT_CONFIG_OPTION_SYSTEM_ZLIB
/*************************************************************************/
/* */
/* Bzip2-compressed file support. */
/* */
/* FreeType now handles font files that have been compressed with the */
/* `bzip2' program. This is mostly used to parse many of the PCF */
/* files that come with XFree86. The implementation uses `libbz2' to */
/* partially uncompress the file on the fly (see src/bzip2/ftbzip2.c). */
/* Contrary to gzip, bzip2 currently is not included and need to use */
/* the system available bzip2 implementation. */
/* */
/* Define this macro if you want to enable this `feature'. */
/* */
/* If you use a build system like cmake or the `configure' script, */
/* options set by those programs have precendence, overwriting the */
/* value here with the configured one. */
/* */
#define FT_CONFIG_OPTION_USE_BZIP2
/*************************************************************************/
/* */
/* Define to disable the use of file stream functions and types, FILE, */
/* fopen() etc. Enables the use of smaller system libraries on embedded */
/* systems that have multiple system libraries, some with or without */
/* file stream support, in the cases where file stream support is not */
/* necessary such as memory loading of font files. */
/* */
/* #define FT_CONFIG_OPTION_DISABLE_STREAM_SUPPORT */
/*************************************************************************/
/* */
/* PNG bitmap support. */
/* */
/* FreeType now handles loading color bitmap glyphs in the PNG format. */
/* This requires help from the external libpng library. Uncompressed */
/* color bitmaps do not need any external libraries and will be */
/* supported regardless of this configuration. */
/* */
/* Define this macro if you want to enable this `feature'. */
/* */
/* If you use a build system like cmake or the `configure' script, */
/* options set by those programs have precendence, overwriting the */
/* value here with the configured one. */
/* */
/* #undef FT_CONFIG_OPTION_USE_PNG */
/*************************************************************************/
/* */
/* HarfBuzz support. */
/* */
/* FreeType uses the HarfBuzz library to improve auto-hinting of */
/* OpenType fonts. If available, many glyphs not directly addressable */
/* by a font's character map will be hinted also. */
/* */
/* Define this macro if you want to enable this `feature'. */
/* */
/* If you use a build system like cmake or the `configure' script, */
/* options set by those programs have precendence, overwriting the */
/* value here with the configured one. */
/* */
/* #undef FT_CONFIG_OPTION_USE_HARFBUZZ */
/*************************************************************************/
/* */
/* Glyph Postscript Names handling */
/* */
/* By default, FreeType 2 is compiled with the `psnames' module. This */
/* module is in charge of converting a glyph name string into a */
/* Unicode value, or return a Macintosh standard glyph name for the */
/* use with the TrueType `post' table. */
/* */
/* Undefine this macro if you do not want `psnames' compiled in your */
/* build of FreeType. This has the following effects: */
/* */
/* - The TrueType driver will provide its own set of glyph names, */
/* if you build it to support postscript names in the TrueType */
/* `post' table, but will not synthesize a missing Unicode charmap. */
/* */
/* - The Type 1 driver will not be able to synthesize a Unicode */
/* charmap out of the glyphs found in the fonts. */
/* */
/* You would normally undefine this configuration macro when building */
/* a version of FreeType that doesn't contain a Type 1 or CFF driver. */
/* */
#define FT_CONFIG_OPTION_POSTSCRIPT_NAMES
/*************************************************************************/
/* */
/* Postscript Names to Unicode Values support */
/* */
/* By default, FreeType 2 is built with the `PSNames' module compiled */
/* in. Among other things, the module is used to convert a glyph name */
/* into a Unicode value. This is especially useful in order to */
/* synthesize on the fly a Unicode charmap from the CFF/Type 1 driver */
/* through a big table named the `Adobe Glyph List' (AGL). */
/* */
/* Undefine this macro if you do not want the Adobe Glyph List */
/* compiled in your `PSNames' module. The Type 1 driver will not be */
/* able to synthesize a Unicode charmap out of the glyphs found in the */
/* fonts. */
/* */
#define FT_CONFIG_OPTION_ADOBE_GLYPH_LIST
/*************************************************************************/
/* */
/* Support for Mac fonts */
/* */
/* Define this macro if you want support for outline fonts in Mac */
/* format (mac dfont, mac resource, macbinary containing a mac */
/* resource) on non-Mac platforms. */
/* */
/* Note that the `FOND' resource isn't checked. */
/* */
#define FT_CONFIG_OPTION_MAC_FONTS
/*************************************************************************/
/* */
/* Guessing methods to access embedded resource forks */
/* */
/* Enable extra Mac fonts support on non-Mac platforms (e.g. */
/* GNU/Linux). */
/* */
/* Resource forks which include fonts data are stored sometimes in */
/* locations which users or developers don't expected. In some cases, */
/* resource forks start with some offset from the head of a file. In */
/* other cases, the actual resource fork is stored in file different */
/* from what the user specifies. If this option is activated, */
/* FreeType tries to guess whether such offsets or different file */
/* names must be used. */
/* */
/* Note that normal, direct access of resource forks is controlled via */
/* the FT_CONFIG_OPTION_MAC_FONTS option. */
/* */
#ifdef FT_CONFIG_OPTION_MAC_FONTS
#define FT_CONFIG_OPTION_GUESSING_EMBEDDED_RFORK
#endif
/*************************************************************************/
/* */
/* Allow the use of FT_Incremental_Interface to load typefaces that */
/* contain no glyph data, but supply it via a callback function. */
/* This is required by clients supporting document formats which */
/* supply font data incrementally as the document is parsed, such */
/* as the Ghostscript interpreter for the PostScript language. */
/* */
#define FT_CONFIG_OPTION_INCREMENTAL
/*************************************************************************/
/* */
/* The size in bytes of the render pool used by the scan-line converter */
/* to do all of its work. */
/* */
#define FT_RENDER_POOL_SIZE 16384L
/*************************************************************************/
/* */
/* FT_MAX_MODULES */
/* */
/* The maximum number of modules that can be registered in a single */
/* FreeType library object. 32 is the default. */
/* */
#define FT_MAX_MODULES 32
/*************************************************************************/
/* */
/* Debug level */
/* */
/* FreeType can be compiled in debug or trace mode. In debug mode, */
/* errors are reported through the `ftdebug' component. In trace */
/* mode, additional messages are sent to the standard output during */
/* execution. */
/* */
/* Define FT_DEBUG_LEVEL_ERROR to build the library in debug mode. */
/* Define FT_DEBUG_LEVEL_TRACE to build it in trace mode. */
/* */
/* Don't define any of these macros to compile in `release' mode! */
/* */
/* Do not #undef these macros here since the build system might define */
/* them for certain configurations only. */
/* */
/* #define FT_DEBUG_LEVEL_ERROR */
/* #define FT_DEBUG_LEVEL_TRACE */
/*************************************************************************/
/* */
/* Autofitter debugging */
/* */
/* If FT_DEBUG_AUTOFIT is defined, FreeType provides some means to */
/* control the autofitter behaviour for debugging purposes with global */
/* boolean variables (consequently, you should *never* enable this */
/* while compiling in `release' mode): */
/* */
/* _af_debug_disable_horz_hints */
/* _af_debug_disable_vert_hints */
/* _af_debug_disable_blue_hints */
/* */
/* Additionally, the following functions provide dumps of various */
/* internal autofit structures to stdout (using `printf'): */
/* */
/* af_glyph_hints_dump_points */
/* af_glyph_hints_dump_segments */
/* af_glyph_hints_dump_edges */
/* af_glyph_hints_get_num_segments */
/* af_glyph_hints_get_segment_offset */
/* */
/* As an argument, they use another global variable: */
/* */
/* _af_debug_hints */
/* */
/* Please have a look at the `ftgrid' demo program to see how those */
/* variables and macros should be used. */
/* */
/* Do not #undef these macros here since the build system might define */
/* them for certain configurations only. */
/* */
/* #define FT_DEBUG_AUTOFIT */
/*************************************************************************/
/* */
/* Memory Debugging */
/* */
/* FreeType now comes with an integrated memory debugger that is */
/* capable of detecting simple errors like memory leaks or double */
/* deletes. To compile it within your build of the library, you */
/* should define FT_DEBUG_MEMORY here. */
/* */
/* Note that the memory debugger is only activated at runtime when */
/* when the _environment_ variable `FT2_DEBUG_MEMORY' is defined also! */
/* */
/* Do not #undef this macro here since the build system might define */
/* it for certain configurations only. */
/* */
/* #define FT_DEBUG_MEMORY */
/*************************************************************************/
/* */
/* Module errors */
/* */
/* If this macro is set (which is _not_ the default), the higher byte */
/* of an error code gives the module in which the error has occurred, */
/* while the lower byte is the real error code. */
/* */
/* Setting this macro makes sense for debugging purposes only, since */
/* it would break source compatibility of certain programs that use */
/* FreeType 2. */
/* */
/* More details can be found in the files ftmoderr.h and fterrors.h. */
/* */
#undef FT_CONFIG_OPTION_USE_MODULE_ERRORS
/*************************************************************************/
/* */
/* Position Independent Code */
/* */
/* If this macro is set (which is _not_ the default), FreeType2 will */
/* avoid creating constants that require address fixups. Instead the */
/* constants will be moved into a struct and additional intialization */
/* code will be used. */
/* */
/* Setting this macro is needed for systems that prohibit address */
/* fixups, such as BREW. [Note that standard compilers like gcc or */
/* clang handle PIC generation automatically; you don't have to set */
/* FT_CONFIG_OPTION_PIC, which is only necessary for very special */
/* compilers.] */
/* */
/* Note that FT_CONFIG_OPTION_PIC support is not available for all */
/* modules (see `modules.cfg' for a complete list). For building with */
/* FT_CONFIG_OPTION_PIC support, do the following. */
/* */
/* 0. Clone the repository. */
/* 1. Define FT_CONFIG_OPTION_PIC. */
/* 2. Remove all subdirectories in `src' that don't have */
/* FT_CONFIG_OPTION_PIC support. */
/* 3. Comment out the corresponding modules in `modules.cfg'. */
/* 4. Compile. */
/* */
/* #define FT_CONFIG_OPTION_PIC */
/*************************************************************************/
/*************************************************************************/
/**** ****/
/**** S F N T D R I V E R C O N F I G U R A T I O N ****/
/**** ****/
/*************************************************************************/
/*************************************************************************/
/*************************************************************************/
/* */
/* Define TT_CONFIG_OPTION_EMBEDDED_BITMAPS if you want to support */
/* embedded bitmaps in all formats using the SFNT module (namely */
/* TrueType & OpenType). */
/* */
#define TT_CONFIG_OPTION_EMBEDDED_BITMAPS
/*************************************************************************/
/* */
/* Define TT_CONFIG_OPTION_POSTSCRIPT_NAMES if you want to be able to */
/* load and enumerate the glyph Postscript names in a TrueType or */
/* OpenType file. */
/* */
/* Note that when you do not compile the `PSNames' module by undefining */
/* the above FT_CONFIG_OPTION_POSTSCRIPT_NAMES, the `sfnt' module will */
/* contain additional code used to read the PS Names table from a font. */
/* */
/* (By default, the module uses `PSNames' to extract glyph names.) */
/* */
#define TT_CONFIG_OPTION_POSTSCRIPT_NAMES
/*************************************************************************/
/* */
/* Define TT_CONFIG_OPTION_SFNT_NAMES if your applications need to */
/* access the internal name table in a SFNT-based format like TrueType */
/* or OpenType. The name table contains various strings used to */
/* describe the font, like family name, copyright, version, etc. It */
/* does not contain any glyph name though. */
/* */
/* Accessing SFNT names is done through the functions declared in */
/* `ftsnames.h'. */
/* */
#define TT_CONFIG_OPTION_SFNT_NAMES
/*************************************************************************/
/* */
/* TrueType CMap support */
/* */
/* Here you can fine-tune which TrueType CMap table format shall be */
/* supported. */
#define TT_CONFIG_CMAP_FORMAT_0
#define TT_CONFIG_CMAP_FORMAT_2
#define TT_CONFIG_CMAP_FORMAT_4
#define TT_CONFIG_CMAP_FORMAT_6
#define TT_CONFIG_CMAP_FORMAT_8
#define TT_CONFIG_CMAP_FORMAT_10
#define TT_CONFIG_CMAP_FORMAT_12
#define TT_CONFIG_CMAP_FORMAT_13
#define TT_CONFIG_CMAP_FORMAT_14
/*************************************************************************/
/*************************************************************************/
/**** ****/
/**** T R U E T Y P E D R I V E R C O N F I G U R A T I O N ****/
/**** ****/
/*************************************************************************/
/*************************************************************************/
/*************************************************************************/
/* */
/* Define TT_CONFIG_OPTION_BYTECODE_INTERPRETER if you want to compile */
/* a bytecode interpreter in the TrueType driver. */
/* */
/* By undefining this, you will only compile the code necessary to load */
/* TrueType glyphs without hinting. */
/* */
/* Do not #undef this macro here, since the build system might */
/* define it for certain configurations only. */
/* */
#define TT_CONFIG_OPTION_BYTECODE_INTERPRETER
/*************************************************************************/
/* */
/* Define TT_CONFIG_OPTION_SUBPIXEL_HINTING if you want to compile */
/* subpixel hinting support into the TrueType driver. This modifies the */
/* TrueType hinting mechanism when anything but FT_RENDER_MODE_MONO is */
/* requested. */
/* */
/* In particular, it modifies the bytecode interpreter to interpret (or */
/* not) instructions in a certain way so that all TrueType fonts look */
/* like they do in a Windows ClearType (DirectWrite) environment. See */
/* [1] for a technical overview on what this means. See `ttinterp.h' */
/* for more details on the LEAN option. */
/* */
/* There are three possible values. */
/* */
/* Value 1: */
/* This value is associated with the `Infinality' moniker, */
/* contributed by an individual nicknamed Infinality with the goal of */
/* making TrueType fonts render better than on Windows. A high */
/* amount of configurability and flexibility, down to rules for */
/* single glyphs in fonts, but also very slow. Its experimental and */
/* slow nature and the original developer losing interest meant that */
/* this option was never enabled in default builds. */
/* */
/* The corresponding interpreter version is v38. */
/* */
/* Value 2: */
/* The new default mode for the TrueType driver. The Infinality code */
/* base was stripped to the bare minimum and all configurability */
/* removed in the name of speed and simplicity. The configurability */
/* was mainly aimed at legacy fonts like Arial, Times New Roman, or */
/* Courier. Legacy fonts are fonts that modify vertical stems to */
/* achieve clean black-and-white bitmaps. The new mode focuses on */
/* applying a minimal set of rules to all fonts indiscriminately so */
/* that modern and web fonts render well while legacy fonts render */
/* okay. */
/* */
/* The corresponding interpreter version is v40. */
/* */
/* Value 3: */
/* Compile both, making both v38 and v40 available (the latter is the */
/* default). */
/* */
/* By undefining these, you get rendering behavior like on Windows */
/* without ClearType, i.e., Windows XP without ClearType enabled and */
/* Win9x (interpreter version v35). Or not, depending on how much */
/* hinting blood and testing tears the font designer put into a given */
/* font. If you define one or both subpixel hinting options, you can */
/* switch between between v35 and the ones you define (using */
/* `FT_Property_Set'). */
/* */
/* This option requires TT_CONFIG_OPTION_BYTECODE_INTERPRETER to be */
/* defined. */
/* */
/* [1] https://www.microsoft.com/typography/cleartype/truetypecleartype.aspx */
/* */
/* #define TT_CONFIG_OPTION_SUBPIXEL_HINTING 1 */
#define TT_CONFIG_OPTION_SUBPIXEL_HINTING 2
/* #define TT_CONFIG_OPTION_SUBPIXEL_HINTING ( 1 | 2 ) */
/*************************************************************************/
/* */
/* Define TT_CONFIG_OPTION_COMPONENT_OFFSET_SCALED to compile the */
/* TrueType glyph loader to use Apple's definition of how to handle */
/* component offsets in composite glyphs. */
/* */
/* Apple and MS disagree on the default behavior of component offsets */
/* in composites. Apple says that they should be scaled by the scaling */
/* factors in the transformation matrix (roughly, it's more complex) */
/* while MS says they should not. OpenType defines two bits in the */
/* composite flags array which can be used to disambiguate, but old */
/* fonts will not have them. */
/* */
/* https://www.microsoft.com/typography/otspec/glyf.htm */
/* https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6glyf.html */
/* */
#undef TT_CONFIG_OPTION_COMPONENT_OFFSET_SCALED
/*************************************************************************/
/* */
/* Define TT_CONFIG_OPTION_GX_VAR_SUPPORT if you want to include */
/* support for Apple's distortable font technology (fvar, gvar, cvar, */
/* and avar tables). This has many similarities to Type 1 Multiple */
/* Masters support. */
/* */
#define TT_CONFIG_OPTION_GX_VAR_SUPPORT
/*************************************************************************/
/* */
/* Define TT_CONFIG_OPTION_BDF if you want to include support for */
/* an embedded `BDF ' table within SFNT-based bitmap formats. */
/* */
#define TT_CONFIG_OPTION_BDF
/*************************************************************************/
/* */
/* Option TT_CONFIG_OPTION_MAX_RUNNABLE_OPCODES controls the maximum */
/* number of bytecode instructions executed for a single run of the */
/* bytecode interpreter, needed to prevent infinite loops. You don't */
/* want to change this except for very special situations (e.g., making */
/* a library fuzzer spend less time to handle broken fonts). */
/* */
/* It is not expected that this value is ever modified by a configuring */
/* script; instead, it gets surrounded with #ifndef ... #endif so that */
/* the value can be set as a preprocessor option on the compiler's */
/* command line. */
/* */
#ifndef TT_CONFIG_OPTION_MAX_RUNNABLE_OPCODES
#define TT_CONFIG_OPTION_MAX_RUNNABLE_OPCODES 1000000L
#endif
/*************************************************************************/
/*************************************************************************/
/**** ****/
/**** T Y P E 1 D R I V E R C O N F I G U R A T I O N ****/
/**** ****/
/*************************************************************************/
/*************************************************************************/
/*************************************************************************/
/* */
/* T1_MAX_DICT_DEPTH is the maximum depth of nest dictionaries and */
/* arrays in the Type 1 stream (see t1load.c). A minimum of 4 is */
/* required. */
/* */
#define T1_MAX_DICT_DEPTH 5
/*************************************************************************/
/* */
/* T1_MAX_SUBRS_CALLS details the maximum number of nested sub-routine */
/* calls during glyph loading. */
/* */
#define T1_MAX_SUBRS_CALLS 16
/*************************************************************************/
/* */
/* T1_MAX_CHARSTRING_OPERANDS is the charstring stack's capacity. A */
/* minimum of 16 is required. */
/* */
/* The Chinese font MingTiEG-Medium (CNS 11643 character set) needs 256. */
/* */
#define T1_MAX_CHARSTRINGS_OPERANDS 256
/*************************************************************************/
/* */
/* Define this configuration macro if you want to prevent the */
/* compilation of `t1afm', which is in charge of reading Type 1 AFM */
/* files into an existing face. Note that if set, the T1 driver will be */
/* unable to produce kerning distances. */
/* */
#undef T1_CONFIG_OPTION_NO_AFM
/*************************************************************************/
/* */
/* Define this configuration macro if you want to prevent the */
/* compilation of the Multiple Masters font support in the Type 1 */
/* driver. */
/* */
#undef T1_CONFIG_OPTION_NO_MM_SUPPORT
/*************************************************************************/
/* */
/* T1_CONFIG_OPTION_OLD_ENGINE controls whether the pre-Adobe Type 1 */
/* engine gets compiled into FreeType. If defined, it is possible to */
/* switch between the two engines using the `hinting-engine' property of */
/* the type1 driver module. */
/* */
/* #define T1_CONFIG_OPTION_OLD_ENGINE */
/*************************************************************************/
/*************************************************************************/
/**** ****/
/**** C F F D R I V E R C O N F I G U R A T I O N ****/
/**** ****/
/*************************************************************************/
/*************************************************************************/
/*************************************************************************/
/* */
/* Using CFF_CONFIG_OPTION_DARKENING_PARAMETER_{X,Y}{1,2,3,4} it is */
/* possible to set up the default values of the four control points that */
/* define the stem darkening behaviour of the (new) CFF engine. For */
/* more details please read the documentation of the */
/* `darkening-parameters' property (file `ftdriver.h'), which allows the */
/* control at run-time. */
/* */
/* Do *not* undefine these macros! */
/* */
#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1 500
#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1 400
#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 1000
#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2 275
#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 1667
#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3 275
#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4 2333
#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4 0
/*************************************************************************/
/* */
/* CFF_CONFIG_OPTION_OLD_ENGINE controls whether the pre-Adobe CFF */
/* engine gets compiled into FreeType. If defined, it is possible to */
/* switch between the two engines using the `hinting-engine' property of */
/* the cff driver module. */
/* */
/* #define CFF_CONFIG_OPTION_OLD_ENGINE */
/*************************************************************************/
/*************************************************************************/
/**** ****/
/**** P C F D R I V E R C O N F I G U R A T I O N ****/
/**** ****/
/*************************************************************************/
/*************************************************************************/
/*************************************************************************/
/* */
/* There are many PCF fonts just called `Fixed' which look completely */
/* different, and which have nothing to do with each other. When */
/* selecting `Fixed' in KDE or Gnome one gets results that appear rather */
/* random, the style changes often if one changes the size and one */
/* cannot select some fonts at all. This option makes the PCF module */
/* prepend the foundry name (plus a space) to the family name. */
/* */
/* We also check whether we have `wide' characters; all put together, we */
/* get family names like `Sony Fixed' or `Misc Fixed Wide'. */
/* */
/* If this option is activated, it can be controlled with the */
/* `no-long-family-names' property of the pcf driver module. */
/* */
/* #define PCF_CONFIG_OPTION_LONG_FAMILY_NAMES */
/*************************************************************************/
/*************************************************************************/
/**** ****/
/**** A U T O F I T M O D U L E C O N F I G U R A T I O N ****/
/**** ****/
/*************************************************************************/
/*************************************************************************/
/*************************************************************************/
/* */
/* Compile autofit module with CJK (Chinese, Japanese, Korean) script */
/* support. */
/* */
#define AF_CONFIG_OPTION_CJK
/*************************************************************************/
/* */
/* Compile autofit module with fallback Indic script support, covering */
/* some scripts that the `latin' submodule of the autofit module doesn't */
/* (yet) handle. */
/* */
#define AF_CONFIG_OPTION_INDIC
/*************************************************************************/
/* */
/* Compile autofit module with warp hinting. The idea of the warping */
/* code is to slightly scale and shift a glyph within a single dimension */
/* so that as much of its segments are aligned (more or less) on the */
/* grid. To find out the optimal scaling and shifting value, various */
/* parameter combinations are tried and scored. */
/* */
/* This experimental option is active only if the rendering mode is */
/* FT_RENDER_MODE_LIGHT; you can switch warping on and off with the */
/* `warping' property of the auto-hinter (see file `ftdriver.h' for more */
/* information; by default it is switched off). */
/* */
#define AF_CONFIG_OPTION_USE_WARPER
/*************************************************************************/
/* */
/* Use TrueType-like size metrics for `light' auto-hinting. */
/* */
/* It is strongly recommended to avoid this option, which exists only to */
/* help some legacy applications retain its appearance and behaviour */
/* with respect to auto-hinted TrueType fonts. */
/* */
/* The very reason this option exists at all are GNU/Linux distributions */
/* like Fedora that did not un-patch the following change (which was */
/* present in FreeType between versions 2.4.6 and 2.7.1, inclusive). */
/* */
/* 2011-07-16 Steven Chu <steven.f.chu@gmail.com> */
/* */
/* [truetype] Fix metrics on size request for scalable fonts. */
/* */
/* This problematic commit is now reverted (more or less). */
/* */
/* #define AF_CONFIG_OPTION_TT_SIZE_METRICS */
/* */
/*
* This macro is obsolete. Support has been removed in FreeType
* version 2.5.
*/
/* #define FT_CONFIG_OPTION_OLD_INTERNALS */
/*
* This macro is defined if native TrueType hinting is requested by the
* definitions above.
*/
#ifdef TT_CONFIG_OPTION_BYTECODE_INTERPRETER
#define TT_USE_BYTECODE_INTERPRETER
#ifdef TT_CONFIG_OPTION_SUBPIXEL_HINTING
#if TT_CONFIG_OPTION_SUBPIXEL_HINTING & 1
#define TT_SUPPORT_SUBPIXEL_HINTING_INFINALITY
#endif
#if TT_CONFIG_OPTION_SUBPIXEL_HINTING & 2
#define TT_SUPPORT_SUBPIXEL_HINTING_MINIMAL
#endif
#endif
#endif
/*
* Check CFF darkening parameters. The checks are the same as in function
* `cff_property_set' in file `cffdrivr.c'.
*/
#if CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1 < 0 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 < 0 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 < 0 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4 < 0 || \
\
CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1 < 0 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2 < 0 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3 < 0 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4 < 0 || \
\
CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1 > \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 > \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 > \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4 || \
\
CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1 > 500 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2 > 500 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3 > 500 || \
CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4 > 500
#error "Invalid CFF darkening parameters!"
#endif
FT_END_HEADER
#endif /* FTOPTION_H_ */
/* END */
@@ -0,0 +1,175 @@
/***************************************************************************/
/* */
/* ftstdlib.h */
/* */
/* ANSI-specific library and header configuration file (specification */
/* only). */
/* */
/* Copyright 2002-2018 by */
/* David Turner, Robert Wilhelm, and Werner Lemberg. */
/* */
/* This file is part of the FreeType project, and may only be used, */
/* modified, and distributed under the terms of the FreeType project */
/* license, LICENSE.TXT. By continuing to use, modify, or distribute */
/* this file you indicate that you have read the license and */
/* understand and accept it fully. */
/* */
/***************************************************************************/
/*************************************************************************/
/* */
/* This file is used to group all #includes to the ANSI C library that */
/* FreeType normally requires. It also defines macros to rename the */
/* standard functions within the FreeType source code. */
/* */
/* Load a file which defines FTSTDLIB_H_ before this one to override it. */
/* */
/*************************************************************************/
#ifndef FTSTDLIB_H_
#define FTSTDLIB_H_
#include <stddef.h>
#define ft_ptrdiff_t ptrdiff_t
/**********************************************************************/
/* */
/* integer limits */
/* */
/* UINT_MAX and ULONG_MAX are used to automatically compute the size */
/* of `int' and `long' in bytes at compile-time. So far, this works */
/* for all platforms the library has been tested on. */
/* */
/* Note that on the extremely rare platforms that do not provide */
/* integer types that are _exactly_ 16 and 32 bits wide (e.g. some */
/* old Crays where `int' is 36 bits), we do not make any guarantee */
/* about the correct behaviour of FT2 with all fonts. */
/* */
/* In these case, `ftconfig.h' will refuse to compile anyway with a */
/* message like `couldn't find 32-bit type' or something similar. */
/* */
/**********************************************************************/
#include <limits.h>
#define FT_CHAR_BIT CHAR_BIT
#define FT_USHORT_MAX USHRT_MAX
#define FT_INT_MAX INT_MAX
#define FT_INT_MIN INT_MIN
#define FT_UINT_MAX UINT_MAX
#define FT_LONG_MIN LONG_MIN
#define FT_LONG_MAX LONG_MAX
#define FT_ULONG_MAX ULONG_MAX
/**********************************************************************/
/* */
/* character and string processing */
/* */
/**********************************************************************/
#include <string.h>
#define ft_memchr memchr
#define ft_memcmp memcmp
#define ft_memcpy memcpy
#define ft_memmove memmove
#define ft_memset memset
#define ft_strcat strcat
#define ft_strcmp strcmp
#define ft_strcpy strcpy
#define ft_strlen strlen
#define ft_strncmp strncmp
#define ft_strncpy strncpy
#define ft_strrchr strrchr
#define ft_strstr strstr
/**********************************************************************/
/* */
/* file handling */
/* */
/**********************************************************************/
#include <stdio.h>
#define FT_FILE FILE
#define ft_fclose fclose
#define ft_fopen fopen
#define ft_fread fread
#define ft_fseek fseek
#define ft_ftell ftell
#define ft_sprintf sprintf
/**********************************************************************/
/* */
/* sorting */
/* */
/**********************************************************************/
#include <stdlib.h>
#define ft_qsort qsort
/**********************************************************************/
/* */
/* memory allocation */
/* */
/**********************************************************************/
#define ft_scalloc calloc
#define ft_sfree free
#define ft_smalloc malloc
#define ft_srealloc realloc
/**********************************************************************/
/* */
/* miscellaneous */
/* */
/**********************************************************************/
#define ft_strtol strtol
#define ft_getenv getenv
/**********************************************************************/
/* */
/* execution control */
/* */
/**********************************************************************/
#include <setjmp.h>
#define ft_jmp_buf jmp_buf /* note: this cannot be a typedef since */
/* jmp_buf is defined as a macro */
/* on certain platforms */
#define ft_longjmp longjmp
#define ft_setjmp( b ) setjmp( *(ft_jmp_buf*) &(b) ) /* same thing here */
/* the following is only used for debugging purposes, i.e., if */
/* FT_DEBUG_LEVEL_ERROR or FT_DEBUG_LEVEL_TRACE are defined */
#include <stdarg.h>
#endif /* FTSTDLIB_H_ */
/* END */
@@ -0,0 +1,187 @@
/***************************************************************************/
/* */
/* ftadvanc.h */
/* */
/* Quick computation of advance widths (specification only). */
/* */
/* Copyright 2008-2018 by */
/* David Turner, Robert Wilhelm, and Werner Lemberg. */
/* */
/* This file is part of the FreeType project, and may only be used, */
/* modified, and distributed under the terms of the FreeType project */
/* license, LICENSE.TXT. By continuing to use, modify, or distribute */
/* this file you indicate that you have read the license and */
/* understand and accept it fully. */
/* */
/***************************************************************************/
#ifndef FTADVANC_H_
#define FTADVANC_H_
#include <ft2build.h>
#include FT_FREETYPE_H
#ifdef FREETYPE_H
#error "freetype.h of FreeType 1 has been loaded!"
#error "Please fix the directory search order for header files"
#error "so that freetype.h of FreeType 2 is found first."
#endif
FT_BEGIN_HEADER
/**************************************************************************
*
* @section:
* quick_advance
*
* @title:
* Quick retrieval of advance values
*
* @abstract:
* Retrieve horizontal and vertical advance values without processing
* glyph outlines, if possible.
*
* @description:
* This section contains functions to quickly extract advance values
* without handling glyph outlines, if possible.
*
* @order:
* FT_Get_Advance
* FT_Get_Advances
*
*/
/*************************************************************************/
/* */
/* <Const> */
/* FT_ADVANCE_FLAG_FAST_ONLY */
/* */
/* <Description> */
/* A bit-flag to be OR-ed with the `flags' parameter of the */
/* @FT_Get_Advance and @FT_Get_Advances functions. */
/* */
/* If set, it indicates that you want these functions to fail if the */
/* corresponding hinting mode or font driver doesn't allow for very */
/* quick advance computation. */
/* */
/* Typically, glyphs that are either unscaled, unhinted, bitmapped, */
/* or light-hinted can have their advance width computed very */
/* quickly. */
/* */
/* Normal and bytecode hinted modes that require loading, scaling, */
/* and hinting of the glyph outline, are extremely slow by */
/* comparison. */
/* */
#define FT_ADVANCE_FLAG_FAST_ONLY 0x20000000L
/*************************************************************************/
/* */
/* <Function> */
/* FT_Get_Advance */
/* */
/* <Description> */
/* Retrieve the advance value of a given glyph outline in an */
/* @FT_Face. */
/* */
/* <Input> */
/* face :: The source @FT_Face handle. */
/* */
/* gindex :: The glyph index. */
/* */
/* load_flags :: A set of bit flags similar to those used when */
/* calling @FT_Load_Glyph, used to determine what kind */
/* of advances you need. */
/* <Output> */
/* padvance :: The advance value. If scaling is performed (based on */
/* the value of `load_flags'), the advance value is in */
/* 16.16 format. Otherwise, it is in font units. */
/* */
/* If @FT_LOAD_VERTICAL_LAYOUT is set, this is the */
/* vertical advance corresponding to a vertical layout. */
/* Otherwise, it is the horizontal advance in a */
/* horizontal layout. */
/* */
/* <Return> */
/* FreeType error code. 0 means success. */
/* */
/* <Note> */
/* This function may fail if you use @FT_ADVANCE_FLAG_FAST_ONLY and */
/* if the corresponding font backend doesn't have a quick way to */
/* retrieve the advances. */
/* */
/* A scaled advance is returned in 16.16 format but isn't transformed */
/* by the affine transformation specified by @FT_Set_Transform. */
/* */
FT_EXPORT( FT_Error )
FT_Get_Advance( FT_Face face,
FT_UInt gindex,
FT_Int32 load_flags,
FT_Fixed *padvance );
/*************************************************************************/
/* */
/* <Function> */
/* FT_Get_Advances */
/* */
/* <Description> */
/* Retrieve the advance values of several glyph outlines in an */
/* @FT_Face. */
/* */
/* <Input> */
/* face :: The source @FT_Face handle. */
/* */
/* start :: The first glyph index. */
/* */
/* count :: The number of advance values you want to retrieve. */
/* */
/* load_flags :: A set of bit flags similar to those used when */
/* calling @FT_Load_Glyph. */
/* */
/* <Output> */
/* padvance :: The advance values. This array, to be provided by the */
/* caller, must contain at least `count' elements. */
/* */
/* If scaling is performed (based on the value of */
/* `load_flags'), the advance values are in 16.16 format. */
/* Otherwise, they are in font units. */
/* */
/* If @FT_LOAD_VERTICAL_LAYOUT is set, these are the */
/* vertical advances corresponding to a vertical layout. */
/* Otherwise, they are the horizontal advances in a */
/* horizontal layout. */
/* */
/* <Return> */
/* FreeType error code. 0 means success. */
/* */
/* <Note> */
/* This function may fail if you use @FT_ADVANCE_FLAG_FAST_ONLY and */
/* if the corresponding font backend doesn't have a quick way to */
/* retrieve the advances. */
/* */
/* Scaled advances are returned in 16.16 format but aren't */
/* transformed by the affine transformation specified by */
/* @FT_Set_Transform. */
/* */
FT_EXPORT( FT_Error )
FT_Get_Advances( FT_Face face,
FT_UInt start,
FT_UInt count,
FT_Int32 load_flags,
FT_Fixed *padvances );
/* */
FT_END_HEADER
#endif /* FTADVANC_H_ */
/* END */
Loaded 100 of 232 files, more files were not shown because too many files have changed in this diff. Show more