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 <https://github.com/Genymobile/scrcpy/pull/6766>
This commit is contained in:
Romain Vimont 2026-04-10 19:48:03 +02:00
parent 4f97e2e30b
commit 3c3cfe8cc6
5 changed files with 61 additions and 54 deletions

View file

@ -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() {

View file

@ -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 {

View file

@ -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);
}

View file

@ -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

View file

@ -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));
}
}