diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a..430d870e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -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,23 @@ 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 = "[x][/]", + .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.\n" + "If / is provided, use that fixed density.", + }, + { + .longopt_id = OPT_ADAPTIVE_SCALE, + .longopt = "adaptive-scale", + .argdesc = "", + .text = "Set adaptive scale factor (dpi = scale * 160). " + "Default is 1.0 (dpi=160).", + }, { .longopt_id = OPT_NO_AUDIO, .longopt = "no-audio", @@ -1804,6 +1823,29 @@ parse_display_id(const char *s, uint32_t *display_id) { return true; } +static bool +parse_display_dpi(const char *s, uint16_t *dpi_out, bool *has_dpi_out) { + *has_dpi_out = false; + if (!s) { + return true; + } + const char *slash = strrchr(s, '/'); + if (!slash) { + return true; + } + const char *dpi_str = slash + 1; + if (*dpi_str == '\0') { + return false; + } + long value = 0; + if (!sc_str_parse_integer(dpi_str, &value) || value <= 0 || value > 65535) { + return false; + } + *dpi_out = (uint16_t) value; + *has_dpi_out = true; + return true; +} + static bool parse_log_level(const char *s, enum sc_log_level *log_level) { if (!strcmp(s, "verbose")) { @@ -2797,6 +2839,31 @@ 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 : ""; + if (optarg) { + uint16_t dpi = 0; + bool has_dpi = false; + if (!parse_display_dpi(optarg, &dpi, &has_dpi)) { + LOGE("Invalid adaptive-new-display DPI: %s", optarg); + return false; + } + if (has_dpi) { + opts->adaptive_dpi = dpi; + } + } + 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; diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e46c6165..4391e3ef 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -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; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 74dbcba8..38a0426c 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -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; }; }; diff --git a/app/src/options.c b/app/src/options.c index 0fe82d29..9d71c447 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -109,6 +109,9 @@ const struct scrcpy_options scrcpy_options_default = { .mouse_hover = true, .audio_dup = false, .new_display = NULL, + .adaptive_new_display = false, + .adaptive_scale = 0.0, + .adaptive_dpi = 0, .start_app = NULL, .angle = NULL, .vd_destroy_content = true, diff --git a/app/src/options.h b/app/src/options.h index 03b42913..1a6f77d7 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -324,6 +324,9 @@ struct scrcpy_options { bool mouse_hover; bool audio_dup; const char *new_display; // [x][/] parsed by the server + bool adaptive_new_display; + double adaptive_scale; + uint16_t adaptive_dpi; // if non-zero, use as fixed dpi for adaptive mode const char *start_app; bool vd_destroy_content; bool vd_system_decorations; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aedfdf9c..0e277406 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -824,6 +824,10 @@ 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, + .adaptive_dpi = options->adaptive_dpi, }; if (!sc_screen_init(&s->screen, &screen_params)) { diff --git a/app/src/screen.c b/app/src/screen.c index da17df0e..f1543bf2 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -4,12 +4,14 @@ #include #include +#include "control_msg.h" #include "events.h" #include "icon.h" #include "options.h" #include "util/log.h" #define DISPLAY_MARGINS 96 +#define SC_VD_RESIZE_DEBOUNCE_MS 250 #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) @@ -26,6 +28,41 @@ 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->vd_fixed_dpi) { + return screen->vd_fixed_dpi; + } + 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 +76,58 @@ 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 < 64 || size.height < 64) { + return; + } + screen->vd_resize_size = size; + screen->vd_resize_pending = true; + screen->vd_resize_deadline_ms = SDL_GetTicks() + + SC_VD_RESIZE_DEBOUNCE_MS; +} + +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 +422,16 @@ 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_fixed_dpi = params->adaptive_dpi; + screen->vd_resize_deadline_ms = 0; + screen->vd_scale = params->adaptive_scale; screen->video = params->video; @@ -491,9 +590,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 +669,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 +791,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 @@ -811,6 +936,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 +963,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; diff --git a/app/src/screen.h b/app/src/screen.h index 6621b2d2..50c35499 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -69,6 +69,19 @@ 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; + uint16_t vd_fixed_dpi; + uint32_t vd_resize_deadline_ms; + double vd_scale; }; struct sc_screen_params { @@ -100,6 +113,10 @@ struct sc_screen_params { bool fullscreen; bool start_fps_counter; + bool new_display; + bool adaptive_new_display; + double adaptive_scale; + uint16_t adaptive_dpi; }; // initialize screen, create window, renderer and texture (window is hidden) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index 0eb96adc..34dbfaea 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -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; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 830a7ec7..d79f0cf4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -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(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b4a8e3ca..11f5e1e0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -331,6 +331,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 } @@ -754,4 +757,15 @@ 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); + nd.setDisplaySize(width, height, dpi); + } else { + Ln.w("Display resize ignored: not a virtual display capture"); + } + } } 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 e933f60e..69b31c23 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -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,30 @@ 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, keeping existing display", e); + } + } + + invalidate(); + } }