Compare commits

...

10 Commits

Author SHA1 Message Date
Cameron Gutman 3bcd2ee068 Ignore bogus refresh rates just to be on the safe side 2018-02-04 15:26:40 -08:00
Cameron Gutman d4ff58b3ad Version 5.6.5 2018-02-04 12:59:52 -08:00
Cameron Gutman c797318ece Use frame drop hack to reduce latency and micro-stuttering for now 2018-02-04 12:59:04 -08:00
Cameron Gutman 82387d23f8 Send client's display refresh rate to server for better frame pacing 2018-02-03 22:09:42 -08:00
Cameron Gutman 766e629be5 Use applicationId com.limelight.unofficial for release builds by default 2018-02-03 19:45:18 -08:00
Cameron Gutman b93aa42c0c Fix detection on HEVC support on some buggy devices 2018-01-28 21:16:28 -08:00
Cameron Gutman 36f132942f Version 5.6.4 2018-01-20 15:13:17 -08:00
Cameron Gutman e4c251e7ee Ignore NVIDIA mouse capture extension on root builds to avoid broken LineageOS implementation 2018-01-20 02:31:40 -08:00
Cameron Gutman fb54bd5c78 Send the initial number of connected gamepads during launch to fix some games like L4D2 2018-01-20 01:16:25 -08:00
Cameron Gutman 8d4c86e113 Update common to support sending initial gamepads and fixing WoL 2018-01-20 01:11:39 -08:00
8 changed files with 189 additions and 27 deletions
+34 -2
View File
@@ -11,8 +11,8 @@ android {
minSdkVersion 16
targetSdkVersion 27
versionName "5.6.3"
versionCode = 142
versionName "5.6.5"
versionCode = 145
}
flavorDimensions "root"
@@ -54,6 +54,38 @@ android {
applicationIdSuffix ".debug"
}
release {
// To whomever is releasing/using an APK in release mode with
// Moonlight's official application ID, please stop. I see every
// single one of your crashes in my Play Console and it makes
// Moonlight's reliability look worse and makes it more difficult
// to distinguish real crashes from your crashy VR app. Seriously,
// 44 of the *same* native crash in 72 hours and a few each of
// several other crashes.
//
// This is technically not your fault. I would have hoped Google
// would validate the signature of the APK before attributing
// the crash to it. I asked their Play Store support about this
// and they said they don't and don't have plans to, so that sucks.
//
// In any case, it's bad form to release an APK using someone
// else's application ID. There is no legitimate reason, that
// anyone would need to comment out the following line, except me
// when I release an official signed Moonlight build. If you feel
// like doing so would solve something, I can tell you it will not.
// You can't upgrade an app while retaining data without having the
// same signature as the official version. Nor can you post it on
// the Play Store, since that application ID is already taken.
// Reputable APK hosting websites similarly validate the signature
// is consistent with the Play Store and won't allow an APK that
// isn't signed the same as the original.
//
// I wish any and all people using Moonlight as the basis of other
// cool projects the best of luck with their efforts. All I ask
// is to please change the applicationId before you publish.
//
// TL;DR: Leave the following line alone!
applicationIdSuffix ".unofficial"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
+57 -5
View File
@@ -294,7 +294,50 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
Toast.makeText(this, "No H.265 decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show();
}
int gamepadMask = ControllerHandler.getAttachedControllerMask(this);
if (!prefConfig.multiController && gamepadMask != 0) {
// If any gamepads are present in non-MC mode, set only gamepad 1.
gamepadMask = 1;
}
if (prefConfig.onscreenController) {
// If we're using OSC, always set at least gamepad 1.
gamepadMask |= 1;
}
// Set to the optimal mode for streaming
float displayRefreshRate = prepareDisplayForRendering();
LimeLog.info("Display refresh rate: "+displayRefreshRate);
// HACK: Despite many efforts to ensure low latency consistent frame
// delivery, the best non-lossy mechanism is to buffer 1 extra frame
// in the output pipeline. Android does some buffering on its end
// in SurfaceFlinger and it's difficult (impossible?) to inspect
// the precise state of the buffer queue to the screen after we
// release a frame for rendering.
//
// Since buffering a frame adds latency and we are primarily a
// latency-optimized client, rather than one designed for picture-perfect
// accuracy, we will synthetically induce a negative pressure on the display
// output pipeline by driving the decoder input pipeline under the speed
// that the display can refresh. This ensures a constant negative pressure
// to keep latency down but does induce a periodic frame loss. However, this
// periodic frame loss is *way* less than what we'd already get in Marshmallow's
// display pipeline where frames are dropped outside of our control if they land
// on the same V-sync.
//
// Hopefully, we can get rid of this once someone comes up with a better way
// to track the state of the pipeline and time frames.
int roundedRefreshRate = Math.round(displayRefreshRate);
if (roundedRefreshRate <= 49) {
// Let's avoid clearly bogus refresh rates
roundedRefreshRate = 60;
}
if (!prefConfig.disableFrameDrop && prefConfig.fps >= roundedRefreshRate) {
prefConfig.fps = roundedRefreshRate - 1;
LimeLog.info("Adjusting FPS target for screen to "+prefConfig.fps);
}
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
@@ -307,6 +350,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
.setHevcBitratePercentageMultiplier(75)
.setHevcSupported(decoderRenderer.isHevcSupported())
.setEnableHdr(willStreamHdr)
.setAttachedGamepadMask(gamepadMask)
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
.setAudioConfiguration(prefConfig.enable51Surround ?
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
MoonBridge.AUDIO_CONFIGURATION_STEREO)
@@ -319,9 +364,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(controllerHandler, null);
// Set to the optimal mode for streaming
prepareDisplayForRendering();
// Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i,
@@ -396,9 +438,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
private void prepareDisplayForRendering() {
private float prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
float displayRefreshRate;
// On M, we can explicitly set the optimal display mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -436,6 +479,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
displayRefreshRate = bestMode.getRefreshRate();
}
// On L, we can at least tell the OS that we want 60 Hz
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -448,6 +492,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
displayRefreshRate = bestRefreshRate;
}
else {
// Otherwise, the active display refresh rate is just
// whatever is currently in use.
displayRefreshRate = display.getRefreshRate();
}
// Apply the display mode change
@@ -483,6 +533,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Set the surface to scale based on the aspect ratio of the stream
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
}
return displayRefreshRate;
}
@SuppressLint("InlinedApi")
@@ -2,6 +2,8 @@ package com.limelight.binding.input;
import android.content.Context;
import android.hardware.input.InputManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.InputDevice;
@@ -12,6 +14,7 @@ import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
@@ -48,7 +51,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private boolean hasGameController;
private final boolean multiControllerEnabled;
private short currentControllers;
private short currentControllers, initialControllers;
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
this.activityContext = activityContext;
@@ -102,6 +105,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// consume these. Instead, let's ignore them since that's probably the
// most likely case.
defaultContext.ignoreBack = true;
// Get the initially attached set of gamepads. As each gamepad receives
// its initial InputEvent, we will move these from this set onto the
// currentControllers set which will allow them to properly unplug
// if they are removed.
initialControllers = getAttachedControllerMask(activityContext);
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
@@ -139,8 +148,47 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
onInputDeviceAdded(deviceId);
}
public static short getAttachedControllerMask(Context context) {
int count = 0;
short mask = 0;
// Count all input devices that are gamepads
InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
for (int id : im.getInputDeviceIds()) {
InputDevice dev = im.getInputDevice(id);
if (dev == null) {
continue;
}
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0) {
LimeLog.info("Counting InputDevice: "+dev.getName());
mask |= 1 << count++;
}
}
// Count all USB devices that match our drivers
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
for (UsbDevice dev : usbManager.getDeviceList().values()) {
if (UsbDriverService.shouldClaimDevice(dev)) {
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
mask |= 1 << count++;
}
}
LimeLog.info("Enumerated "+count+" gamepads");
return mask;
}
private void releaseControllerNumber(GenericControllerContext context) {
// If this device sent data as a gamepad, zero the values before removing
// If we reserved a controller number, remove that reservation
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
}
// If this device sent data as a gamepad, zero the values before removing.
// We must do this after clearing the currentControllers entry so this
// causes the device to be removed on the server PC.
if (context.assignedControllerNumber) {
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
(short) 0,
@@ -148,12 +196,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
(short) 0, (short) 0,
(short) 0, (short) 0);
}
// If we reserved a controller number, remove that reservation
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
}
}
// Called before sending input but after we've determined that this
@@ -181,6 +223,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
// Take this value out of the initial gamepad set
initialControllers &= ~(1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
@@ -201,6 +247,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
// Take this value out of the initial gamepad set
initialControllers &= ~(1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
@@ -473,7 +523,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private short getActiveControllerMask() {
if (multiControllerEnabled) {
return currentControllers;
return (short)(currentControllers | initialControllers);
}
else {
// Only Player 1 is active with multi-controller disabled
@@ -3,6 +3,7 @@ package com.limelight.binding.input.capture;
import android.app.Activity;
import com.limelight.LimeLog;
import com.limelight.LimelightBuildProps;
import com.limelight.R;
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
import com.limelight.binding.input.evdev.EvdevListener;
@@ -13,7 +14,9 @@ public class InputCaptureManager {
LimeLog.info("Using Android O+ native mouse capture");
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
}
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
else if (!LimelightBuildProps.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using NVIDIA mouse capture extension");
return new ShieldCaptureProvider(activity);
}
@@ -157,7 +157,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
private boolean isRecognizedInputDevice(UsbDevice device) {
private static boolean isRecognizedInputDevice(UsbDevice device) {
// On KitKat and later, we can determine if this VID and PID combo
// matches an existing input device and defer to the built-in controller
// support in that case. Prior to KitKat, we'll always return true to be safe.
@@ -182,7 +182,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
private boolean shouldClaimDevice(UsbDevice device) {
public static boolean shouldClaimDevice(UsbDevice device) {
// We always bind to XB1 controllers but only bind to XB360 controllers
// if we know the kernel isn't already driving this device.
return XboxOneController.canClaimDevice(device) ||
@@ -410,7 +410,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
// Render the last buffer
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && prefs.disableFrameDrop) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Use a PTS that will cause this frame to never be dropped if frame dropping
// is disabled
videoDecoder.releaseOutputBuffer(lastIndex, 0);
@@ -153,27 +153,51 @@ public class MediaCodecHelper {
// Qualcomm is currently the only decoders in this group.
}
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
private static String getAdrenoVersionString(String glRenderer) {
glRenderer = glRenderer.toLowerCase().trim();
if (!glRenderer.contains("adreno")) {
return false;
return null;
}
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
Matcher matcher = modelNumberPattern.matcher(glRenderer);
if (!matcher.matches()) {
return false;
return null;
}
String modelNumber = matcher.group(2);
LimeLog.info("Found Adreno GPU: "+modelNumber);
return modelNumber;
}
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
String modelNumber = getAdrenoVersionString(glRenderer);
if (modelNumber == null) {
// Not an Adreno GPU
return false;
}
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
return modelNumber.charAt(1) == '0';
}
// This is a workaround for some broken devices that report
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
// An example of such a device is the Huawei Honor 5x with the
// Snapdragon 616 SoC (Adreno 405).
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
String modelNumber = getAdrenoVersionString(glRenderer);
if (modelNumber == null) {
// Not an Adreno GPU
return false;
}
// Snapdragon 4xx and higher support GLES 3.1
return modelNumber.charAt(0) >= '4';
}
public static void initialize(Context context, String glRenderer) {
if (initialized) {
return;
@@ -208,9 +232,10 @@ public class MediaCodecHelper {
// 3xx - bad
// 4xx - good
//
// Unfortunately, it's not that easy to get that information here, so I'll use an
// approximation by checking the GLES level (<= 3.0 is bad).
if (configInfo.reqGlEsVersion > 0x30000) {
// The "good" GPUs support GLES 3.1, but we can't just check that directly
// (see comment on isGLES31SnapdragonRenderer).
//
if (isGLES31SnapdragonRenderer(glRenderer)) {
// We prefer reference frame invalidation support (which is only doable on AVC on
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use