From 3c3cfe8cc6e9ca0ff9f0a10eb03789e0abfc7812 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Apr 2026 19:48:03 +0200 Subject: [PATCH] Constraint size and alignment simultaneously Previously, the size was scaled to fit the requested maximum size and then aligned. If the maximum size was not a multiple of the alignment, the resulting size was suboptimal as it preserved the aspect ratio less accurately. This also prepares for additional video constraints. PR #6766 --- .../com/genymobile/scrcpy/device/Size.java | 66 ++++++------------- .../scrcpy/video/CameraCapture.java | 2 +- .../scrcpy/video/NewDisplayCapture.java | 10 +-- .../scrcpy/video/ScreenCapture.java | 2 +- .../genymobile/scrcpy/device/SizeTest.java | 35 ++++++++++ 5 files changed, 61 insertions(+), 54 deletions(-) create mode 100644 server/src/test/java/com/genymobile/scrcpy/device/SizeTest.java diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index b684a54e..53ff607a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -29,59 +29,35 @@ public final class Size { return new Size(height, width); } - public Size limit(int maxSize) { + public Size constrain(int maxSize, int alignment) { assert maxSize >= 0 : "Max size may not be negative"; + assert alignment > 0 : "Alignment must be positive"; + assert (alignment & (alignment - 1)) == 0 : "Alignment must be a power-of-two"; - if (maxSize == 0) { - // No limit - return this; + int alignedMaxSize = maxSize / alignment * alignment; // round to a multiple of alignment + int w, h; + + if (maxSize > 0 && (width > alignedMaxSize || height > alignedMaxSize)) { + if (width > height) { + w = alignedMaxSize; + h = round(height * alignedMaxSize / width, alignment); + } else { + w = round(width * alignedMaxSize / height, alignment); + h = alignedMaxSize; + } + } else { + w = round(width, alignment); + h = round(height, alignment); } - boolean portrait = height > width; - int major = portrait ? height : width; - if (major <= maxSize) { - return this; - } + assert maxSize == 0 || w <= maxSize : "The width cannot exceed maxSize if maxSize is aligned"; + assert maxSize == 0 || h <= maxSize : "The height cannot exceed maxSize if maxSize is aligned"; - int minor = portrait ? width : height; - - int newMajor = maxSize; - int newMinor = maxSize * minor / major; - - int w = portrait ? newMinor : newMajor; - int h = portrait ? newMajor : newMinor; return new Size(w, h); } - /** - * Round both dimensions of this size to be multiples of {@code alignment}. - * - * @param alignment the required alignment - * @return the current size rounded - */ - public Size round(int alignment) { - if (isMultipleOf(alignment)) { - // Already aligned - return this; - } - - boolean portrait = height > width; - int major = portrait ? height : width; - int minor = portrait ? width : height; - - major = major / alignment * alignment; // round down to not exceed the initial size - minor = (minor + (alignment / 2)) / alignment * alignment; // round to the nearest to minimize aspect ratio distortion - if (minor > major) { - minor = major; - } - - int w = portrait ? minor : major; - int h = portrait ? major : minor; - return new Size(w, h); - } - - public boolean isMultipleOf(int alignment) { - return width % alignment == 0 && height % alignment == 0; + private static int round(int value, int alignment) { + return (value + (alignment / 2)) / alignment * alignment; } public Rect toRect() { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index b9fbd9b7..d23481f0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -149,7 +149,7 @@ public class CameraCapture extends SurfaceCapture { filter.addAngle(angle); transform = filter.getInverseTransform(); - videoSize = filter.getOutputSize().limit(maxSize).round(getAlignment()); + videoSize = filter.getOutputSize().constrain(maxSize, getAlignment()); } private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException { 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 cc9e8c6e..289d6bb6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -133,13 +133,9 @@ public class NewDisplayCapture extends SurfaceCapture { filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); filter.addAngle(angle); - int alignment = getAlignment(); - Size filteredSize = filter.getOutputSize(); - if (!filteredSize.isMultipleOf(alignment) || (maxSize != 0 && filteredSize.getMax() > maxSize)) { - if (maxSize != 0) { - filteredSize = filteredSize.limit(maxSize); - } - filteredSize = filteredSize.round(alignment); + Size outputSize = filter.getOutputSize(); + Size filteredSize = outputSize.constrain(maxSize, getAlignment()); + if (!filteredSize.equals(outputSize)) { filter.addResize(filteredSize); } 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 81db7159..a7624c8e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -97,7 +97,7 @@ public class ScreenCapture extends SurfaceCapture { filter.addAngle(angle); transform = filter.getInverseTransform(); - videoSize = filter.getOutputSize().limit(maxSize).round(getAlignment()); + videoSize = filter.getOutputSize().constrain(maxSize, getAlignment()); } @Override diff --git a/server/src/test/java/com/genymobile/scrcpy/device/SizeTest.java b/server/src/test/java/com/genymobile/scrcpy/device/SizeTest.java new file mode 100644 index 00000000..7f5dd4b1 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/device/SizeTest.java @@ -0,0 +1,35 @@ +package com.genymobile.scrcpy.device; + +import org.junit.Assert; +import org.junit.Test; + +public class SizeTest { + @Test + public void testConstrainSize() { + Size size = new Size(207, 209); + + Assert.assertEquals(size, size.constrain(0, 1)); + Assert.assertEquals(new Size(208, 210), size.constrain(0, 2)); + Assert.assertEquals(new Size(208, 208), size.constrain(0, 4)); + + Size s = size.constrain(208, 2); + Assert.assertEquals(208, s.getHeight()); + Assert.assertTrue(s.getWidth() >= 206 && s.getWidth() <= 208 && s.getWidth() % 2 == 0); + + Assert.assertEquals(new Size(207 * 208 / 209, 208), size.constrain(208, 2)); + Assert.assertEquals(new Size(208, 208), size.constrain(208, 4)); + } + + @Test + public void testConstrainSizeUnchanged() { + Size size = new Size(256, 512); + + Assert.assertEquals(size, size.constrain(0, 1)); + Assert.assertEquals(size, size.constrain(512, 1)); + Assert.assertEquals(size, size.constrain(515, 1)); + Assert.assertEquals(size, size.constrain(512, 16)); + Assert.assertEquals(size, size.constrain(515, 16)); + Assert.assertEquals(size, size.constrain(0, 16)); + Assert.assertEquals(size, size.constrain(4096, 16)); + } +}