From 03878083fb8e5af806a78a3ad99f401edac47a95 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Apr 2026 10:04:32 +0200 Subject: [PATCH] Reset capture on rotation (fix square displays) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DisplayMonitor` previously only triggered a capture reset when the display size changed. In most cases, rotation also changes dimensions, so the behavior was correct… except for square displays where width and height remain unchanged. However, rotation still requires a capture reset even when dimensions do not change, to ensure the orientation filter is applied so virtual displays are rendered correctly. To reproduce the issue: scrcpy --new-display=600x600 --start-app=com.android.settings Then press Alt+r to rotate the Settings app. PR #6770 --- ...aySizeMonitor.java => DisplayMonitor.java} | 51 +++++++++--------- .../scrcpy/display/DisplayProperties.java | 52 +++++++++++++++++++ .../scrcpy/video/NewDisplayCapture.java | 13 ++--- .../scrcpy/video/ScreenCapture.java | 18 ++++--- 4 files changed, 94 insertions(+), 40 deletions(-) rename server/src/main/java/com/genymobile/scrcpy/display/{DisplaySizeMonitor.java => DisplayMonitor.java} (70%) create mode 100644 server/src/main/java/com/genymobile/scrcpy/display/DisplayProperties.java diff --git a/server/src/main/java/com/genymobile/scrcpy/display/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/display/DisplayMonitor.java similarity index 70% rename from server/src/main/java/com/genymobile/scrcpy/display/DisplaySizeMonitor.java rename to server/src/main/java/com/genymobile/scrcpy/display/DisplayMonitor.java index b8e4e6f2..5a31fff5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/display/DisplaySizeMonitor.java +++ b/server/src/main/java/com/genymobile/scrcpy/display/DisplayMonitor.java @@ -2,7 +2,6 @@ package com.genymobile.scrcpy.display; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.model.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.DisplayWindowListener; @@ -14,10 +13,10 @@ import android.os.Handler; import android.os.HandlerThread; import android.view.IDisplayWindowListener; -public class DisplaySizeMonitor { +public class DisplayMonitor { public interface Listener { - void onDisplaySizeChanged(); + void onDisplayPropertiesChanged(); } // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really @@ -33,7 +32,7 @@ public class DisplaySizeMonitor { private int displayId = Device.DISPLAY_ID_NONE; - private Size sessionDisplaySize; + private DisplayProperties props; private Listener listener; @@ -51,11 +50,11 @@ public class DisplaySizeMonitor { Handler handler = new Handler(handlerThread.getLooper()); displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> { if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")"); + Ln.v("DisplayMonitor: onDisplayChanged(" + eventDisplayId + ")"); } if (eventDisplayId == displayId) { - checkDisplaySizeChanged(); + checkDisplayPropertiesChanged(); } }, handler); } else { @@ -63,11 +62,11 @@ public class DisplaySizeMonitor { @Override public void onDisplayConfigurationChanged(int eventDisplayId, Configuration newConfig) { if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayConfigurationChanged(" + eventDisplayId + ")"); + Ln.v("DisplayMonitor: onDisplayConfigurationChanged(" + eventDisplayId + ")"); } if (eventDisplayId == displayId) { - checkDisplaySizeChanged(); + checkDisplayPropertiesChanged(); } } }; @@ -97,38 +96,38 @@ public class DisplaySizeMonitor { } } - private synchronized Size getAndSetSessionDisplaySize(Size sessionDisplaySize) { - Size oldDisplaySize = this.sessionDisplaySize; - this.sessionDisplaySize = sessionDisplaySize; - return oldDisplaySize; + private synchronized DisplayProperties getAndSetDisplayProperties(DisplayProperties props) { + DisplayProperties oldProps = this.props; + this.props = props; + return oldProps; } - public synchronized void setSessionDisplaySize(Size sessionDisplaySize) { - this.sessionDisplaySize = sessionDisplaySize; + public synchronized void setSessionDisplayProperties(DisplayProperties props) { + this.props = props; } - private void checkDisplaySizeChanged() { + private void checkDisplayPropertiesChanged() { DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId); if (di == null) { Ln.w("DisplayInfo for " + displayId + " cannot be retrieved"); - // We can't compare with the current size, so reset unconditionally - Size oldDisplaySize = getAndSetSessionDisplaySize(null); // exchange with synchronization + // We can't compare with the current properties, so reset unconditionally + DisplayProperties oldProps = getAndSetDisplayProperties(null); // exchange with synchronization if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: requestReset(): " + oldDisplaySize + " -> (unknown)"); + Ln.v("DisplayMonitor: requestReset(): " + oldProps + " -> (unknown)"); } - listener.onDisplaySizeChanged(); + listener.onDisplayPropertiesChanged(); } else { - Size size = di.getSize(); + DisplayProperties newProps = new DisplayProperties(di.getSize(), di.getRotation()); - Size oldDisplaySize = getAndSetSessionDisplaySize(size); // exchange with synchronization - if (!size.equals(oldDisplaySize)) { - // Reset only if the size is different + DisplayProperties oldProps = getAndSetDisplayProperties(newProps); // exchange with synchronization + if (!newProps.equals(oldProps)) { + // Reset only if the properties are different if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: requestReset(): " + oldDisplaySize + " -> " + size); + Ln.v("DisplayMonitor: requestReset(): " + oldProps + " -> " + newProps); } - listener.onDisplaySizeChanged(); + listener.onDisplayPropertiesChanged(); } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: Size not changed (" + size + "): do not requestReset()"); + Ln.v("DisplayMonitor: DisplayProperties not changed (" + newProps + "): do not requestReset()"); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/display/DisplayProperties.java b/server/src/main/java/com/genymobile/scrcpy/display/DisplayProperties.java new file mode 100644 index 00000000..d3658334 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/display/DisplayProperties.java @@ -0,0 +1,52 @@ +package com.genymobile.scrcpy.display; + +import com.genymobile.scrcpy.model.Size; + +import java.util.Objects; + +public final class DisplayProperties { + private Size size; + private int rotation; + + public DisplayProperties(Size size, int rotation) { + assert size != null; + assert rotation >= 0 && rotation < 4; + this.size = size; + this.rotation = rotation; + } + + public Size getSize() { + return size; + } + + public void setSize(Size size) { + this.size = size; + } + + public int getRotation() { + return rotation; + } + + public void setRotation(int rotation) { + this.rotation = rotation; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + DisplayProperties that = (DisplayProperties) o; + return rotation == that.rotation && Objects.equals(size, that.size); + } + + @Override + public int hashCode() { + return Objects.hash(size, rotation); + } + + @Override + public String toString() { + return size + " [rotation=" + rotation + "]"; + } +} 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 ae728772..5608d131 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -4,7 +4,8 @@ import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.display.DisplayInfo; -import com.genymobile.scrcpy.display.DisplaySizeMonitor; +import com.genymobile.scrcpy.display.DisplayMonitor; +import com.genymobile.scrcpy.display.DisplayProperties; import com.genymobile.scrcpy.model.NewDisplay; import com.genymobile.scrcpy.model.Orientation; import com.genymobile.scrcpy.model.Size; @@ -42,7 +43,7 @@ public class NewDisplayCapture extends SurfaceCapture { private final VirtualDisplayListener vdListener; private final NewDisplay newDisplay; - private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); + private final DisplayMonitor displayMonitor = new DisplayMonitor(); private AffineMatrix displayTransform; private AffineMatrix eventTransform; @@ -112,8 +113,8 @@ public class NewDisplayCapture extends SurfaceCapture { } displayRotation = 0; - // Set the current display size to avoid an unnecessary call to invalidate() - displaySizeMonitor.setSessionDisplaySize(displaySize); + // Set the current display properties to avoid an unnecessary call to invalidate() + displayMonitor.setSessionDisplayProperties(new DisplayProperties(displaySize, displayRotation)); } else { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId()); displaySize = displayInfo.getSize(); @@ -194,7 +195,7 @@ public class NewDisplayCapture extends SurfaceCapture { ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); } - displaySizeMonitor.start(virtualDisplayId, this::invalidate); + displayMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { Ln.e("Could not create display", e); throw new AssertionError("Could not create display"); @@ -232,7 +233,7 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public void release() { - displaySizeMonitor.stopAndRelease(); + displayMonitor.stopAndRelease(); if (virtualDisplay != null) { virtualDisplay.release(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 77d2eb17..adabdfb0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -5,7 +5,8 @@ import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.display.DisplayInfo; -import com.genymobile.scrcpy.display.DisplaySizeMonitor; +import com.genymobile.scrcpy.display.DisplayMonitor; +import com.genymobile.scrcpy.display.DisplayProperties; import com.genymobile.scrcpy.model.ConfigurationException; import com.genymobile.scrcpy.model.Orientation; import com.genymobile.scrcpy.model.Size; @@ -38,7 +39,7 @@ public class ScreenCapture extends SurfaceCapture { private DisplayInfo displayInfo; private Size videoSize; - private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); + private final DisplayMonitor displayMonitor = new DisplayMonitor(); private IBinder display; private VirtualDisplay virtualDisplay; @@ -60,7 +61,7 @@ public class ScreenCapture extends SurfaceCapture { @Override public void init() { - displaySizeMonitor.start(displayId, this::invalidate); + displayMonitor.start(displayId, this::invalidate); } @Override @@ -76,23 +77,24 @@ public class ScreenCapture extends SurfaceCapture { } Size displaySize = displayInfo.getSize(); - displaySizeMonitor.setSessionDisplaySize(displaySize); + int displayRotation = displayInfo.getRotation(); + displayMonitor.setSessionDisplayProperties(new DisplayProperties(displaySize, displayRotation)); if (captureOrientationLock == Orientation.Lock.LockedInitial) { // The user requested to lock the video orientation to the current orientation captureOrientationLock = Orientation.Lock.LockedValue; - captureOrientation = Orientation.fromRotation(displayInfo.getRotation()); + captureOrientation = Orientation.fromRotation(displayRotation); } VideoFilter filter = new VideoFilter(displaySize); if (crop != null) { - boolean transposed = (displayInfo.getRotation() % 2) != 0; + boolean transposed = (displayRotation % 2) != 0; filter.addCrop(crop, transposed); } boolean locked = captureOrientationLock != Orientation.Lock.Unlocked; - filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation); + filter.addOrientation(displayRotation, locked, captureOrientation); filter.addAngle(angle); transform = filter.getInverseTransform(); @@ -169,7 +171,7 @@ public class ScreenCapture extends SurfaceCapture { @Override public void release() { - displaySizeMonitor.stopAndRelease(); + displayMonitor.stopAndRelease(); if (display != null) { SurfaceControl.destroyDisplay(display);