Added adaptive virtual display that adjusts to window size

This commit is contained in:
Tinura Dinith 2026-03-05 02:15:28 +05:30
parent 3fcc177da5
commit 52709d60ba
12 changed files with 308 additions and 8 deletions

View file

@ -106,6 +106,8 @@ enum {
OPT_AUDIO_DUP,
OPT_GAMEPAD,
OPT_NEW_DISPLAY,
OPT_ADAPTIVE_NEW_DISPLAY,
OPT_ADAPTIVE_SCALE,
OPT_LIST_APPS,
OPT_START_APP,
OPT_SCREEN_OFF_TIMEOUT,
@ -632,6 +634,22 @@ static const struct sc_option options[] = {
" --new-display # main display size and density\n"
" --new-display=/240 # main display size and 240 dpi",
},
{
.longopt_id = OPT_ADAPTIVE_NEW_DISPLAY,
.longopt = "adaptive-new-display",
.argdesc = "[<width>x<height>][/<dpi>]",
.optional_arg = true,
.text = "Create a new display that adapts to the scrcpy window size.\n"
"When the window is resized, the virtual display is resized "
"accordingly and its density is updated.",
},
{
.longopt_id = OPT_ADAPTIVE_SCALE,
.longopt = "adaptive-scale",
.argdesc = "<scale>",
.text = "Set adaptive scale factor (dpi = scale * 160). "
"Default is 1.0 (dpi=160).",
},
{
.longopt_id = OPT_NO_AUDIO,
.longopt = "no-audio",
@ -2797,6 +2815,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_NEW_DISPLAY:
opts->new_display = optarg ? optarg : "";
break;
case OPT_ADAPTIVE_NEW_DISPLAY:
opts->adaptive_new_display = true;
opts->new_display = optarg ? optarg : "";
break;
case OPT_ADAPTIVE_SCALE: {
char *endptr = NULL;
double value = strtod(optarg, &endptr);
if (!endptr || *endptr != '\0' || value <= 0.0) {
LOGE("Invalid adaptive scale: %s", optarg);
return false;
}
opts->adaptive_scale = value;
break;
}
case OPT_START_APP:
opts->start_app = optarg;
break;

View file

@ -182,6 +182,11 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255);
return 1 + len;
}
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_SIZE:
sc_write16be(&buf[1], msg->set_display_size.width);
sc_write16be(&buf[3], msg->set_display_size.height);
sc_write16be(&buf[5], msg->set_display_size.dpi);
return 7;
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
@ -315,6 +320,12 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_START_APP:
LOG_CMSG("start app \"%s\"", msg->start_app.name);
break;
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_SIZE:
LOG_CMSG("set display size %ux%u/%u",
msg->set_display_size.width,
msg->set_display_size.height,
msg->set_display_size.dpi);
break;
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
LOG_CMSG("reset video");
break;

View file

@ -43,6 +43,7 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
SC_CONTROL_MSG_TYPE_START_APP,
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
SC_CONTROL_MSG_TYPE_SET_DISPLAY_SIZE,
};
enum sc_copy_key {
@ -111,6 +112,11 @@ struct sc_control_msg {
struct {
char *name;
} start_app;
struct {
uint16_t width;
uint16_t height;
uint16_t dpi;
} set_display_size;
};
};

View file

@ -109,6 +109,8 @@ const struct scrcpy_options scrcpy_options_default = {
.mouse_hover = true,
.audio_dup = false,
.new_display = NULL,
.adaptive_new_display = false,
.adaptive_scale = 0.0,
.start_app = NULL,
.angle = NULL,
.vd_destroy_content = true,

View file

@ -324,6 +324,8 @@ struct scrcpy_options {
bool mouse_hover;
bool audio_dup;
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
bool adaptive_new_display;
double adaptive_scale;
const char *start_app;
bool vd_destroy_content;
bool vd_system_decorations;

View file

@ -824,6 +824,9 @@ aoa_complete:
.mipmaps = options->mipmaps,
.fullscreen = options->fullscreen,
.start_fps_counter = options->start_fps_counter,
.new_display = options->new_display != NULL,
.adaptive_new_display = options->adaptive_new_display,
.adaptive_scale = options->adaptive_scale,
};
if (!sc_screen_init(&s->screen, &screen_params)) {

View file

@ -4,6 +4,7 @@
#include <string.h>
#include <SDL2/SDL.h>
#include "control_msg.h"
#include "events.h"
#include "icon.h"
#include "options.h"
@ -26,6 +27,38 @@ get_oriented_size(struct sc_size size, enum sc_orientation orientation) {
return oriented_size;
}
static uint16_t
get_window_dpi(const struct sc_screen *screen) {
if (screen->adaptive_new_display) {
double scale = screen->vd_scale > 0.0 ? screen->vd_scale : 1.0;
uint32_t dpi = (uint32_t) (scale * 160.0 + 0.5);
if (dpi == 0) {
dpi = 1;
} else if (dpi > 65535) {
dpi = 65535;
}
return (uint16_t) dpi;
}
int display_index = SDL_GetWindowDisplayIndex(screen->window);
if (display_index < 0) {
return 0;
}
float ddpi = 0.f;
float hdpi = 0.f;
float vdpi = 0.f;
if (SDL_GetDisplayDPI(display_index, &ddpi, &hdpi, &vdpi) != 0) {
return 0;
}
if (ddpi <= 0.f) {
return 0;
}
if (ddpi > 65535.f) {
ddpi = 65535.f;
}
return (uint16_t) (ddpi + 0.5f);
}
// get the window size in a struct sc_size
static struct sc_size
get_window_size(const struct sc_screen *screen) {
@ -39,6 +72,60 @@ get_window_size(const struct sc_screen *screen) {
return size;
}
static void
sc_screen_schedule_vd_resize(struct sc_screen *screen) {
if (!screen->adaptive_new_display) {
return;
}
if (!screen->vd_resize_enabled) {
return;
}
// screen->im.controller may be NULL if --no-control
if (!screen->im.controller) {
return;
}
struct sc_size size = get_window_size(screen);
if (size.width == 0 || size.height == 0) {
return;
}
if (size.width < 64 || size.height < 64) {
return;
}
screen->vd_resize_size = size;
screen->vd_resize_pending = true;
screen->vd_resize_deadline_ms = SDL_GetTicks() + 250;
}
static void
sc_screen_maybe_send_vd_resize(struct sc_screen *screen) {
if (!screen->vd_resize_pending) {
return;
}
uint32_t now = SDL_GetTicks();
if (now < screen->vd_resize_deadline_ms) {
return;
}
if (screen->vd_last_sent_valid
&& screen->vd_last_sent_size.width == screen->vd_resize_size.width
&& screen->vd_last_sent_size.height == screen->vd_resize_size.height) {
screen->vd_resize_pending = false;
return;
}
uint16_t dpi = get_window_dpi(screen);
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_SIZE,
};
msg.set_display_size.width = (uint16_t) screen->vd_resize_size.width;
msg.set_display_size.height = (uint16_t) screen->vd_resize_size.height;
msg.set_display_size.dpi = dpi;
if (sc_controller_push_msg(screen->im.controller, &msg)) {
screen->vd_last_sent_size = screen->vd_resize_size;
screen->vd_last_sent_valid = true;
screen->vd_last_sent_dpi = dpi;
}
screen->vd_resize_pending = false;
}
static struct sc_point
get_window_position(const struct sc_screen *screen) {
int x;
@ -333,6 +420,15 @@ sc_screen_init(struct sc_screen *screen,
screen->paused = false;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
screen->new_display = params->new_display;
screen->adaptive_new_display = params->adaptive_new_display;
screen->vd_resize_pending = false;
screen->vd_resize_enabled = false;
screen->vd_initial_resize_sent = false;
screen->vd_last_sent_valid = false;
screen->vd_last_sent_dpi = 0;
screen->vd_resize_deadline_ms = 0;
screen->vd_scale = params->adaptive_scale;
screen->video = params->video;
@ -491,9 +587,22 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
int y = screen->req.y != SC_WINDOW_POSITION_UNDEFINED
? screen->req.y : (int) SDL_WINDOWPOS_CENTERED;
struct sc_size window_size =
get_initial_optimal_size(screen->content_size, screen->req.width,
screen->req.height);
struct sc_size window_size;
if (screen->adaptive_new_display
&& !screen->req.width && !screen->req.height) {
struct sc_size bounds;
if (get_preferred_display_bounds(&bounds)) {
window_size = get_optimal_size(bounds, screen->content_size, true);
} else {
window_size = get_initial_optimal_size(screen->content_size,
screen->req.width,
screen->req.height);
}
} else {
window_size = get_initial_optimal_size(screen->content_size,
screen->req.width,
screen->req.height);
}
set_window_size(screen, window_size);
SDL_SetWindowPosition(screen->window, x, y);
@ -557,6 +666,14 @@ static void
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
assert(screen->video);
if (screen->new_display) {
// For new-display, keep the window size controlled by the user.
// Do not auto-resize the window to the content size to avoid
// feedback loops with virtual display resizing.
screen->content_size = new_content_size;
return;
}
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) {
@ -671,6 +788,11 @@ sc_screen_apply_frame(struct sc_screen *screen) {
screen->has_frame = true;
// this is the very first frame, show the window
sc_screen_show_initial_window(screen);
screen->vd_resize_enabled = true;
if (screen->adaptive_new_display && !screen->vd_initial_resize_sent) {
sc_screen_schedule_vd_resize(screen);
screen->vd_initial_resize_sent = true;
}
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse on start
@ -800,6 +922,7 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
sc_screen_maybe_send_vd_resize(screen);
switch (event->type) {
case SC_EVENT_SCREEN_INIT_SIZE: {
// The initial size is passed via screen->frame_size
@ -811,6 +934,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
return true;
}
case SC_EVENT_NEW_FRAME: {
sc_screen_maybe_send_vd_resize(screen);
bool ok = sc_screen_update_frame(screen);
if (!ok) {
LOGE("Frame update failed\n");
@ -837,6 +961,9 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
case SDL_WINDOWEVENT_SIZE_CHANGED:
sc_screen_render(screen, true);
break;
case SDL_WINDOWEVENT_RESIZED:
sc_screen_schedule_vd_resize(screen);
break;
case SDL_WINDOWEVENT_MAXIMIZED:
screen->maximized = true;
break;

View file

@ -69,6 +69,18 @@ struct sc_screen {
bool paused;
AVFrame *resume_frame;
bool new_display;
bool adaptive_new_display;
bool vd_resize_pending;
bool vd_resize_enabled;
bool vd_initial_resize_sent;
struct sc_size vd_resize_size;
struct sc_size vd_last_sent_size;
bool vd_last_sent_valid;
uint16_t vd_last_sent_dpi;
uint32_t vd_resize_deadline_ms;
double vd_scale;
};
struct sc_screen_params {
@ -100,6 +112,9 @@ struct sc_screen_params {
bool fullscreen;
bool start_fps_counter;
bool new_display;
bool adaptive_new_display;
double adaptive_scale;
};
// initialize screen, create window, renderer and texture (window is hidden)

View file

@ -25,6 +25,7 @@ public final class ControlMessage {
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
public static final int TYPE_START_APP = 16;
public static final int TYPE_RESET_VIDEO = 17;
public static final int TYPE_SET_DISPLAY_SIZE = 18;
public static final long SEQUENCE_INVALID = 0;
@ -53,6 +54,9 @@ public final class ControlMessage {
private boolean on;
private int vendorId;
private int productId;
private int displayWidth;
private int displayHeight;
private int displayDpi;
private ControlMessage() {
}
@ -166,6 +170,15 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createSetDisplaySize(int width, int height, int dpi) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_DISPLAY_SIZE;
msg.displayWidth = width;
msg.displayHeight = height;
msg.displayDpi = dpi;
return msg;
}
public int getType() {
return type;
}
@ -249,4 +262,16 @@ public final class ControlMessage {
public int getProductId() {
return productId;
}
public int getDisplayWidth() {
return displayWidth;
}
public int getDisplayHeight() {
return displayHeight;
}
public int getDisplayDpi() {
return displayDpi;
}
}

View file

@ -48,6 +48,8 @@ public class ControlMessageReader {
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case ControlMessage.TYPE_RESET_VIDEO:
return ControlMessage.createEmpty(type);
case ControlMessage.TYPE_SET_DISPLAY_SIZE:
return parseSetDisplaySize();
case ControlMessage.TYPE_UHID_CREATE:
return parseUhidCreate();
case ControlMessage.TYPE_UHID_INPUT:
@ -141,6 +143,13 @@ public class ControlMessageReader {
return ControlMessage.createSetDisplayPower(on);
}
private ControlMessage parseSetDisplaySize() throws IOException {
int width = dis.readUnsignedShort();
int height = dis.readUnsignedShort();
int dpi = dis.readUnsignedShort();
return ControlMessage.createSetDisplaySize(width, height, dpi);
}
private ControlMessage parseUhidCreate() throws IOException {
int id = dis.readUnsignedShort();
int vendorId = dis.readUnsignedShort();

View file

@ -96,6 +96,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
private boolean keepDisplayPowerOff;
private String lastStartedAppPackage;
private boolean lastStartedAppForceStop;
private boolean pendingRelaunchOnResize;
// Used for resetting video encoding on RESET_VIDEO message
private SurfaceCapture surfaceCapture;
@ -146,6 +149,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
displayDataAvailable.notify();
}
}
if (pendingRelaunchOnResize && lastStartedAppPackage != null) {
pendingRelaunchOnResize = false;
Ln.i("Relaunching app \"" + lastStartedAppPackage + "\" on resized display " + virtualDisplayId + "...");
Device.startApp(lastStartedAppPackage, virtualDisplayId, lastStartedAppForceStop);
}
}
public void setSurfaceCapture(SurfaceCapture surfaceCapture) {
@ -331,6 +339,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_RESET_VIDEO:
resetVideo();
break;
case ControlMessage.TYPE_SET_DISPLAY_SIZE:
setDisplaySize(msg.getDisplayWidth(), msg.getDisplayHeight(), msg.getDisplayDpi());
break;
default:
// do nothing
}
@ -691,6 +702,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "...");
Device.startApp(app.getPackageName(), startAppDisplayId, forceStopBeforeStart);
lastStartedAppPackage = app.getPackageName();
lastStartedAppForceStop = forceStopBeforeStart;
}
private int getStartAppDisplayId() {
@ -754,4 +767,18 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
surfaceCapture.requestInvalidate();
}
}
private void setDisplaySize(int width, int height, int dpi) {
if (surfaceCapture instanceof com.genymobile.scrcpy.video.NewDisplayCapture) {
com.genymobile.scrcpy.video.NewDisplayCapture nd =
(com.genymobile.scrcpy.video.NewDisplayCapture) surfaceCapture;
Ln.i("Resize virtual display to " + width + "x" + height + "/" + dpi);
if (lastStartedAppPackage != null) {
pendingRelaunchOnResize = true;
}
nd.setDisplaySize(width, height, dpi);
} else {
Ln.w("Display resize ignored: not a virtual display capture");
}
}
}

View file

@ -64,6 +64,9 @@ public class NewDisplayCapture extends SurfaceCapture {
private Size physicalSize; // the physical size of the display (without rotation)
private int dpi;
private Size requestedDisplaySize;
private int requestedDpi;
private boolean hasRequestedSize;
public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) {
this.vdListener = vdListener;
@ -105,11 +108,20 @@ public class NewDisplayCapture extends SurfaceCapture {
public void prepare() {
int displayRotation;
if (virtualDisplay == null) {
if (!newDisplay.hasExplicitSize()) {
displaySize = mainDisplaySize;
}
if (!newDisplay.hasExplicitDpi()) {
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize);
if (hasRequestedSize && requestedDisplaySize != null) {
displaySize = requestedDisplaySize;
if (requestedDpi != 0) {
dpi = requestedDpi;
} else {
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize);
}
} else {
if (!newDisplay.hasExplicitSize()) {
displaySize = mainDisplaySize;
}
if (!newDisplay.hasExplicitDpi()) {
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize);
}
}
videoSize = displaySize;
@ -266,4 +278,33 @@ public class NewDisplayCapture extends SurfaceCapture {
public void requestInvalidate() {
invalidate();
}
public synchronized void setDisplaySize(int width, int height, int dpi) {
if (width <= 0 || height <= 0) {
return;
}
requestedDisplaySize = new Size(width, height);
requestedDpi = dpi;
hasRequestedSize = true;
if (virtualDisplay != null) {
int newDpi = dpi;
if (newDpi == 0) {
newDpi = scaleDpi(mainDisplaySize, mainDisplayDpi, requestedDisplaySize);
}
try {
virtualDisplay.resize(width, height, newDpi);
displaySizeMonitor.setSessionDisplaySize(requestedDisplaySize);
Ln.i("Virtual display resized in-place to " + width + "x" + height + "/" + newDpi);
} catch (Exception e) {
Ln.w("Virtual display resize failed, fallback to recreate", e);
displaySizeMonitor.stopAndRelease();
virtualDisplay.release();
virtualDisplay = null;
}
}
invalidate();
}
}