Add flex display support (resizable virtual display) - #6772
Conversation
It can be avoided almost entirely with: diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java
index ebe424f95..e2dca2b44 100644
--- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java
@@ -19,6 +19,7 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.graphics.Rect;
import android.hardware.display.VirtualDisplay;
import android.os.Build;
+import android.os.SystemClock;
import android.view.Surface;
import java.io.IOException;
@@ -265,6 +266,7 @@ public class NewDisplayCapture extends SurfaceCapture {
if (virtualDisplay == null) {
startNew(surface);
} else {
+ SystemClock.sleep(300);
virtualDisplay.setSurface(surface);
}
But it's not a really good solution… Even if instead we wait explicitly for the display event before setting the surface, it still glitches. |
|
Oh, it's not necessarily a It can be seen by recording and replaying in slow motion: Then trigger one resize, then replay the file with VLC or mpv in slow motion (use |
|
That's great! I couldn't find any major issues, at least not ones caused by the apps or my phone not handling virtual displays well. |
|
Can it also be used on main display too outside virtual display? Love it btw, Huge thanks. |
Nope, the main display cannot be resized that way (and we don't want to change the resolution of the physical display). |
|
The black bars in landscape mode on main display on pc won't go away I guess right even at the same resolution as phone? |
|
I have a dumb question about glitches (#6772 (comment)), it's probably nonsensical: Could you intentionally drop frames between requesting a resize and setting the new surface? Is it equivalent to your tentative sleep? Would it lead to stuttering (instead of glitches) if it takes too much time? And would the next few frames still show the same issue (#6772 (comment))? |
|
By the way, the documentation of Thus maybe you should do this in Surface oldSurface = virtualDisplay.getSurface();
virtualDisplay.setSurface(surface);
if (oldSurface != null) {
oldSurface.release();
}But I'm really not sure because I don't fully understand how and when virtual displays are (re)created. [EDIT] The doc is not really clear because "detached" is when you set the surface to null, but what happens when you just change it? Is it released automatically? |
|
For physical displays, instead of changing physical display resolution, maybe it can change capture resolution instead. Would be useful when streaming over Internet, or reduce CPU/RAM usages when multiple instances are open (tiled, each one is small) |
|
@anotheruserofgithub I really appreciate the detailed feedback, it's super helpful. I fixed in a new version.
The surface is owned by It seems forcing an OpenGL filter improves the result (not sure if it's just a side effect), but I think it is still not perfect: diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java
index 851be5579..8ad77fb7a 100644
--- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java
@@ -213,6 +213,10 @@ public class NewDisplayCapture extends SurfaceCapture {
// = DISPLAY_FILTER_MATRIX⁻¹ * FILTER_MATRIX⁻¹
// = displayRotationMatrix * eventTransform
displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform);
+ if (flexDisplay && displayTransform == null) {
+ // Force OpenGL rendering to avoid glitches on resize
+ displayTransform = AffineMatrix.IDENTITY;
+ }
}
public void startNew(Surface surface) {
@@ -282,6 +286,9 @@ public class NewDisplayCapture extends SurfaceCapture {
glRunner.stopAndRelease();
glRunner = null;
}
+ if (virtualDisplay != null) {
+ virtualDisplay.setSurface(null);
+ }
}
@Override |
That's an idea for a separate feature (although the quality is lower if you scale the capture because there is no mipmapping), but it won't allow to change the aspect ratio anyway. Not sure this needs to be dynamic (I don't know)? |
Apparently you'd need to turn off window animation, either in Developer options or programmatically: But I couldn't find a way to get the window shown on the virtual display (is there any?) and change its layout params. |
It can be turned off globally (and it improves the visual result): diffdiff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java
index 7ba5cc06d..8d2c1de4c 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java
@@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.IInterface;
+import android.provider.Settings;
import android.view.IDisplayWindowListener;
import java.lang.reflect.Method;
@@ -40,6 +41,7 @@ public final class WindowManager {
private WindowManager(IInterface manager) {
this.manager = manager;
+ disableAnimations();
}
private Method getGetRotationMethod() throws NoSuchMethodException {
@@ -264,4 +266,22 @@ public final class WindowManager {
Ln.e("Could not invoke method", e);
}
}
+
+ public void disableAnimations() {
+ try {
+ Method getMethod = manager.getClass().getMethod("getAnimationScale", int.class);
+ // Settings.Global.WINDOW_ANIMATION_SCALE
+ Ln.i(" [0] = " + getMethod.invoke(manager, 0));
+ // Settings.Global.TRANSITION_ANIMATION_SCALE
+ Ln.i(" [1] = " + getMethod.invoke(manager, 1));
+ // Settings.Global.ANIMATOR_DURATION_SCALE
+ Ln.i(" [2] = " + getMethod.invoke(manager, 2));
+ Method method = manager.getClass().getMethod("setAnimationScale", int.class, float.class);
+ method.invoke(manager, 0, 0);
+ method.invoke(manager, 1, 0);
+ method.invoke(manager, 2, 0);
+ } catch (ReflectiveOperationException e) {
+ Ln.e("Could not invoke method", e);
+ }
+ }
}(the relevant value is at index 1, the "transition animation scale") They can also be set via adb: # get values
adb shell settings get global window_animation_scale
adb shell settings get global transition_animation_scale
adb shell settings get global animator_duration_scale
# disable
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
# restore
adb shell settings put global window_animation_scale 1
adb shell settings put global transition_animation_scale 1
adb shell settings put global animator_duration_scale 1That does not resolve all glitches though. |
This other thing I saw was |
I finally abandoned this approach: In the end, I implemented an event debouncer (based on timing, something I initially wanted to avoid) that applies a resize (by calling In case you're wondering, the debouncer is implemented on the server side (not on the client side) for several reasons:
|
|
I installed a Genymotion emulator and built this PR. I can resize the window a couple times but it always crashes with a buffer dequeue exception (expand to see the logs). This only happens with Terminal outputSide remark: Maybe the "disconnected" icon should be positioned at the center of the window regardless of the render-fit mode? |
Is it specific to the new version of this PR ( |
It fails the same way with all versions, including |
|
See [EDIT] The try-catch avoids the crash (the exception message is "Pending dequeue output buffer request cancelled"), but then the issue is that the virtual display is not resized. It gets resized only when there are no such exceptions. |
I don't know if it's an emulator issue, or if it can happen on a real device. If it cannot happen on real devices, it's not a big deal I think. You edited #6772 (comment) to include verbose logs, but the exception is not exactly the same as the initial one. Initially it included this error message: Btw, can you reproduce with |
Add an option to configure how the rendering fits the window. The default, `--render-fit=letterbox`, preserves the aspect ratio and fits the window as best as possible, adding black bars at the top/bottom or left/right if needed. This has been the only behavior scrcpy supported so far. Another mode, `--render-fit=unscaled`, renders the display without scaling. This mode will be useful for virtual display resizing. Refs #6772 comment <#6772 (review)> PR #6772 <#6772>
The physical size of the virtual display does not change when the display is rotated, although the reported display size does. Refs #6772 comment <#6772 (review)> PR #6772 <#6772>
Refs #6772 comment <#6772 (comment)> PR #6772 <#6772>
Previously, the minor dimension was rounded to the nearest multiple of the alignment requirement when constraining size. This could result in the dimension being increased. Change the behavior to always round down instead, ensuring the constrained size never grows. This fixes a conflict with the client-side "optimal window size" computation, which never increases dimensions. With flex displays, the old behavior could lead to feedback loop between window and display resizing with mismatched dimensions: - window resized to 2341x1317 - display resized to 2340x1318 - window resized to 2338x1317 - display resized to 2336x1318 PR #6772 <#6772>
Make the minimum codec size respect the provided `--min-size-alignment` value. Refs #6766 comment <#6766 (comment)> PR #6772 <#6772>
The size must be constrained by the video capabilities, but unlike fixed displays, the aspect ratio should not be preserved in order to use the maximum available area. Refs #6772 comment <#6772 (comment)> PR #6772 <#6772>
The `--max-size` option behaves slightly differently depending on the mode. Refs #6772 comment <#6772 (comment)> PR #6772 <#6772>
On a computer with a scale factor different from 1, physical and logical sizes differ. For example, with a scale factor of 2, if the logical (window) size is 800x600, the physical (renderer) size is 1600x1200. They were not interpreted consistently in scrcpy. To fix the confusion: - resize the flex display according to the logical size (SDL_EVENT_WINDOW_RESIZED); - scale rendering to match the physical area defined by the logical size; - no longer convert input event coordinates. This was not an issue before flex displays because rendering scaled the content to fit the window (`--render-fit=letterbox`), so the difference in physical size had no effect. Refs #6772 comment <#6772 (comment)> PR #6772 <#6772>
|
Time to merge 🚀 Many thanks again to @anotheruserofgithub for the deep-dive review and thorough testing, which helped catch important issues. 👍 |
|
Great work! Thanks. :) Two things left to do now that this is merged:
|
Refs #6772 comment <#6772 (comment)> Refs #6776 comment <#6776 (comment)>
anotheruserofgithub
left a comment
There was a problem hiding this comment.
Some minor details I was about to post just when you merged, I thought they wouldn't be worth the trouble so I abandoned them, but since I already "revived" another closed PR (#6822 (review)), maybe you might want to consider these as well.
The `dpi` field is not synchronized, and the DPI to use is the one from the latest `DisplayInfo`. Refs #6772 comment <#6772 (comment)>
For consistency, name the "client resize" flag 'R' and add the "frame header" arrow to make the schemas in `app/src/demuxer.c` and `doc/develop.md` identical. Refs #6772 comment <https://github.com/Genymobile/scrcpy/pull/6772/changes#r3305729151> Refs #6772 comment <https://github.com/Genymobile/scrcpy/pull/6772/changes#r3305729425>
For consistency, name the "client resize" flag 'R' and add the "frame header" arrow to make the schemas in `app/src/demuxer.c` and `doc/develop.md` identical. Refs #6772 comment <#6772 (comment)> Refs #6772 comment <#6772 (comment)>
|
this is crazy good for app development |
Use
--keep-activeto prevent the screen from turning off:Increase the bit rate (and/or change the codec) to maintain good quality even with large windows:
Demo
Here is Firefox for Android running in a "flex" virtual display, ran as follow:
scrcpy-flex-display-2.mp4
previous video
scrcpy-flex-display.mp4
Download binaries
Here are binaries built by Github Actions (for
flex-display.16): https://github.com/rom1v/scrcpy/actions/runs/25569279750(download the artifact
release-XXXwhereXXXis your target platform)old versions
flex-display.1: https://github.com/rom1v/scrcpy/actions/runs/24530523954flex-display.4: https://github.com/rom1v/scrcpy/actions/runs/24731426812flex-display.5: https://github.com/rom1v/scrcpy/actions/runs/24899984361flex-display.6: https://github.com/rom1v/scrcpy/actions/runs/25006302461flex-display.7: https://github.com/rom1v/scrcpy/actions/runs/25009172508flex-display.10: https://github.com/rom1v/scrcpy/actions/runs/25065382694flex-display.13: https://github.com/rom1v/scrcpy/actions/runs/25282088772flex-display.15: https://github.com/rom1v/scrcpy/actions/runs/25405231193flex-display.16: https://github.com/rom1v/scrcpy/actions/runs/25508659976Preparation
To prepare compatibility between dynamic resizing and encoders constraints (minimum size, maximum size and alignment), several changes were merged:
Principles
The core of this feature (and the easy part) consists in a call to
VirtualDisplay.resize()."Resize display" requests between the client and the server must never accumulate. To achieve this:
virtualDisplay.resize()is called from the same thread as the encoding process (otherwise Android would internally accumulate resize calls)The difficult part is correctly handling resize events both on the client side and server sides.
In particular, a virtual display can be resized "on its own" (e.g., on app rotation, such as with Alt+r) or as the result of an asynchronous client resize request. Both cases trigger the same resize event (detected by
DeviceMonitor) on the server side, but only independent resizes must reset the capture/encoding session.On the client side, a window resize event triggers a resize request to the device, which (asynchronously) causes the frame size to change later, which in turn may trigger another client window resize…
To handle this properly, the cause of a capture reset is tracked (in particular "client resize" vs "independent resize", see
DisplayPropertiesTracker) and transmitted over the wire as an additional flag in the session metadata introduced in #6159. When a new frame with a new size is received, the client can determine whether it must adapt the window size to match the frame. To avoid stuttering, the window must not be resized if the frame size change resulted from its own resize request, since it's asynchronous and additional resize requests may already be in flight.On the client side, when
--flex-displayis enabled, the rendered frame is not scaled/centered in the window (see--render-fit). It is rendered 1:1 in the top-left corner (which may show black bars or cropping between the resize request and the actual resize, due to unavoidable asynchrony).Glitches
During a display resize, the captured video stream may contain glitches. The issue arises because everything is asynchronous, involves multiple Android processes, and cannot be synchronized/atomic:
virtualDisplay.resize()virtualDisplay.setSurface()In other words, resizing the display and assigning the
MediaCodecSurfaceto the virtual display cannot be made atomic. As a result, the system may briefly render at the old size on the new surface, or at the new size on the old surface.EDIT: also see comments below (#6772 (comment)).
Size and DPI
During a resize, the DPI is preserved. I think it's the correct thing to do.
It is possible to specify the initial size and DPI (e.g.,
--new-display=1920x1080/240). When not specified, the default size is 1280x960 and the default dpi is 160 (arbitrarily). Unlike "normal" mirroring mode, these values are not derived from the device display, as they are tied to the client machine.In theory, they could be computed from the computer's display size and DPI, but this would add complexity and require initializing the SDL video module before starting the server (at least if we want to pass these data as parameter), which would slightly time-to-firstframe. I think a default size and DPI are good enough, as they can still be explicitly configured.
Unlike other PRs, there is no "render factor". The virtual display is rendered 1:1 without scaling, for better quality and simplicity.
PR History
flex-display.1: initial versionflex-display.2: rename--render-fit=naturalto--render-fit=letterboxflex-display.3: fixes after reviewsflex-display.4: change approach (see Add flex display support (resizable virtual display) #6772 (comment))flex-display.5: minor refactors and rebase onto the latestdevflex-display.6: fixes after review + rebase ontodevwith--keep-activeflex-display.7: fix resize-to-fit and wrong timing logicflex-display.8: fix rotation of non-flex displaysflex-display.9: fix resize behavior above maximum codec sizeflex-display.10: rebase on Fix OpenGL runner shutdown deadlock #6794 to fix OpenGL graceful shutdownflex-display.11: rebase and minor fixesflex-display.12: allow--max-sizewith flex displaysflex-display.13: fix behavior for scale factor != 100%flex-display.14: fix video constraints synchronizationflex-display.15: rebase on Add option to change the background color #6807 (dark background) + center unscaled displayflex-display.16: fix rotated virtual display size detectionflex-display.17: center for resize_to_fitflex-display.18: minor technical changes after reviewsSupersedes #6350, #6351 and #6705.
Fixes #6632