From 526e4a81ed6262a4ac8c467631f0aad9b06abb53 Mon Sep 17 00:00:00 2001 From: Reynaldo San Juan Date: Sat, 13 Sep 2025 15:45:45 +0200 Subject: [PATCH 1/8] [#663] feat: resizable virtual display --- app/src/cli.c | 7 +- app/src/control_msg.c | 7 + app/src/control_msg.h | 5 + app/src/scrcpy.c | 4 + app/src/screen.c | 155 +++++++++++++++++- app/src/screen.h | 22 ++- .../java/com/genymobile/scrcpy/Options.java | 12 +- .../scrcpy/control/ControlMessage.java | 19 +++ .../scrcpy/control/ControlMessageReader.java | 8 + .../genymobile/scrcpy/control/Controller.java | 18 ++ .../genymobile/scrcpy/device/NewDisplay.java | 15 +- .../scrcpy/video/NewDisplayCapture.java | 52 +++++- .../scrcpy/video/SurfaceEncoder.java | 12 +- 13 files changed, 323 insertions(+), 13 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a..c5230521 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -621,14 +621,17 @@ static const struct sc_option options[] = { { .longopt_id = OPT_NEW_DISPLAY, .longopt = "new-display", - .argdesc = "[x][/]", + .argdesc = "[x][/][:r]", .optional_arg = true, .text = "Create a new display with the specified resolution and " "density. If not provided, they default to the main display " - "dimensions and DPI.\n" + "dimensions and DPI. Add ':r' to make the display resizable.\n" "Examples:\n" " --new-display=1920x1080\n" " --new-display=1920x1080/420 # force 420 dpi\n" + " --new-display=1920x1080/420:r # resizable display\n" + " --new-display=:r # resizable with main display size\n" + " --new-display=/420:r # resizable with main display size and 420 dpi\n" " --new-display # main display size and density\n" " --new-display=/240 # main display size and 240 dpi", }, diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e46c6165..c9d7439f 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -182,6 +182,10 @@ 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_RESIZE_DISPLAY: + sc_write16be(&buf[1], msg->resize_display.width); + sc_write16be(&buf[3], msg->resize_display.height); + return 5; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -318,6 +322,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_RESET_VIDEO: LOG_CMSG("reset video"); break; + case SC_CONTROL_MSG_TYPE_RESIZE_DISPLAY: + LOG_CMSG("resize display to %dx%d", msg->resize_display.width, msg->resize_display.height); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 74dbcba8..0c216688 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_RESIZE_DISPLAY, }; enum sc_copy_key { @@ -111,6 +112,10 @@ struct sc_control_msg { struct { char *name; } start_app; + struct { + uint16_t width; + uint16_t height; + } resize_display; }; }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aedfdf9c..44bdcb3c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -802,6 +802,9 @@ aoa_complete: const char *window_title = options->window_title ? options->window_title : info->device_name; + // Check if new_display is resizable (contains :r) + bool resizable_new_display = options->new_display && strstr(options->new_display, ":r") != NULL; + struct sc_screen_params screen_params = { .video = options->video_playback, .controller = controller, @@ -824,6 +827,7 @@ aoa_complete: .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, .start_fps_counter = options->start_fps_counter, + .resizable_new_display = resizable_new_display, }; if (!sc_screen_init(&s->screen, &screen_params)) { diff --git a/app/src/screen.c b/app/src/screen.c index da17df0e..006cbc3c 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -11,8 +11,18 @@ #define DISPLAY_MARGINS 96 +// --- Fix: Ensure these are defined before any use --- +#define RESIZE_FINISHED_DELAY 200 +static Uint32 resize_timer_callback(Uint32 interval, void *param); + #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) +// Prototipo para evitar error de declaración implícita +static bool sizes_are_close(int a, int b, int tolerance); + +static void +sc_screen_send_resize_display(struct sc_screen *screen, int width, int height); + static inline struct sc_size get_oriented_size(struct sc_size size, enum sc_orientation orientation) { struct sc_size oriented_size; @@ -239,12 +249,47 @@ static int event_watcher(void *data, SDL_Event *event) { struct sc_screen *screen = data; assert(screen->video); - if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) { - // In practice, it seems to always be called from the same thread in - // that specific case. Anyway, it's just a workaround. - sc_screen_render(screen, true); + int width = event->window.data1; + int height = event->window.data2; + if (screen->resizable_new_display) { + if ((!sizes_are_close(width, screen->last_window_width, 2) || !sizes_are_close(height, screen->last_window_height, 2)) && + !screen->initial_setup && !screen->content_driven_resize) { + screen->last_window_width = width; + screen->last_window_height = height; + // --- Resize diferido --- + screen->pending_resize_width = width; + screen->pending_resize_height = height; + if (screen->resize_timer) { + SDL_RemoveTimer(screen->resize_timer); + } + screen->resize_timer = SDL_AddTimer(RESIZE_FINISHED_DELAY, resize_timer_callback, screen); + // --- Fin resize diferido --- + sc_screen_render(screen, true); + } else if (screen->initial_setup) { + LOGD("[RESIZED] Initial setup resize ignored: %dx%d", width, height); + sc_screen_render(screen, true); + } else if (screen->content_driven_resize) { + LOGD("[RESIZED] Content-driven resize ignored: %dx%d", width, height); + sc_screen_render(screen, true); + } + } else { + // Modo normal + if (screen->content_driven_resize) { + LOGD("[RESIZED] Content-driven resize, solo render"); + sc_screen_render(screen, true); + return 0; + } + if (screen->initial_setup) { + LOGD("[RESIZED] Initial setup, solo render"); + sc_screen_render(screen, true); + return 0; + } + LOGD("[RESIZED] User resize detected: %dx%d", width, height); + sc_screen_render(screen, true); + sc_screen_send_resize_display(screen, width, height); + } } return 0; } @@ -333,6 +378,9 @@ sc_screen_init(struct sc_screen *screen, screen->paused = false; screen->resume_frame = NULL; screen->orientation = SC_ORIENTATION_0; + screen->initial_setup = true; + screen->content_driven_resize = false; + screen->last_resize_time = 0; screen->video = params->video; @@ -343,6 +391,8 @@ sc_screen_init(struct sc_screen *screen, screen->req.fullscreen = params->fullscreen; screen->req.start_fps_counter = params->start_fps_counter; + screen->resizable_new_display = params->resizable_new_display; + bool ok = sc_frame_buffer_init(&screen->fb); if (!ok) { return false; @@ -508,6 +558,10 @@ sc_screen_show_initial_window(struct sc_screen *screen) { SDL_ShowWindow(screen->window); sc_screen_update_content_rect(screen); + + // Mark initial setup as complete - now user resizing will trigger display resize + LOGD("Initial setup complete, enabling user resize detection"); + screen->initial_setup = false; } void @@ -537,11 +591,20 @@ sc_screen_destroy(struct sc_screen *screen) { sc_frame_buffer_destroy(&screen->fb); } +// Utilidad para comparar tamaños con tolerancia +static bool sizes_are_close(int a, int b, int tolerance) { + return abs(a - b) <= tolerance; +} + static void resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, struct sc_size new_content_size) { assert(screen->video); - + if (screen->resizable_new_display) { + // En modo redimensionable, el display nunca debe modificar la ventana + // Por lo tanto, ignorar cualquier resize proveniente del display + return; + } struct sc_size window_size = get_window_size(screen); struct sc_size target_size = { .width = (uint32_t) window_size.width * new_content_size.width @@ -550,7 +613,11 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, / old_content_size.height, }; target_size = get_optimal_size(target_size, new_content_size, true); + + // Mark that we're doing a content-driven resize to avoid feedback loop + screen->content_driven_resize = true; set_window_size(screen, target_size); + screen->content_driven_resize = false; } static void @@ -577,8 +644,11 @@ apply_pending_resize(struct sc_screen *screen) { assert(!screen->maximized); assert(!screen->minimized); if (screen->resize_pending) { + // Mark that we're doing a content-driven resize to avoid feedback loop + screen->content_driven_resize = true; resize_for_content(screen, screen->windowed_content_size, screen->content_size); + screen->content_driven_resize = false; screen->resize_pending = false; } } @@ -800,6 +870,17 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { bool sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { + // --- Manejo especial para SDL_USEREVENT de resize diferido --- + if (event->type == SDL_USEREVENT && event->user.code == 0x5343525A && event->user.data1 == screen) { + // Timer expirado: notificar resize + if (screen->pending_resize_width > 0 && screen->pending_resize_height > 0) { + LOGD("[RESIZE_FINISHED] Notifying display resize: %dx%d", screen->pending_resize_width, screen->pending_resize_height); + sc_screen_send_resize_display(screen, screen->pending_resize_width, screen->pending_resize_height); + } + screen->resize_timer = 0; + return true; + } + // --- Fin manejo especial --- switch (event->type) { case SC_EVENT_SCREEN_INIT_SIZE: { // The initial size is passed via screen->frame_size @@ -835,6 +916,27 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_SIZE_CHANGED: + if (screen->resizable_new_display) { + int w, h; + SDL_GetWindowSize(screen->window, &w, &h); + if ((!sizes_are_close(w, screen->last_window_width, 2) || !sizes_are_close(h, screen->last_window_height, 2)) && + !screen->initial_setup && !screen->content_driven_resize) { + screen->last_window_width = w; + screen->last_window_height = h; + // --- Resize diferido --- + screen->pending_resize_width = w; + screen->pending_resize_height = h; + if (screen->resize_timer) { + SDL_RemoveTimer(screen->resize_timer); + } + screen->resize_timer = SDL_AddTimer(RESIZE_FINISHED_DELAY, resize_timer_callback, screen); + // --- Fin resize diferido --- + } else if (screen->initial_setup) { + LOGD("[SIZE_CHANGED] Initial setup resize ignored: %dx%d", w, h); + } else if (screen->content_driven_resize) { + LOGD("[SIZE_CHANGED] Content-driven resize ignored: %dx%d", w, h); + } + } sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_MAXIMIZED: @@ -871,9 +973,31 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { return true; } +static void +sc_screen_send_resize_display(struct sc_screen *screen, int width, int height) { + if (!screen->im.controller) { + return; + } + + // Validate minimum size constraints to avoid Android errors + if (width < 1 || height < 1) { + LOGD("Display size too small, ignoring resize: %dx%d", width, height); + return; + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_RESIZE_DISPLAY; + msg.resize_display.width = (uint16_t) width; + msg.resize_display.height = (uint16_t) height; + + if (!sc_controller_push_msg(screen->im.controller, &msg)) { + LOGW("Could not request display resize to %dx%d", width, height); + } +} + struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, - int32_t x, int32_t y) { + int32_t x, int32_t y) { assert(screen->video); enum sc_orientation orientation = screen->orientation; @@ -945,3 +1069,22 @@ sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y) { *x = (int64_t) *x * dw / ww; *y = (int64_t) *y * dh / wh; } + +// --- Resize diferido --- +static Uint32 resize_timer_callback(Uint32 interval, void *param) { + (void)interval; + struct sc_screen *screen = param; + assert(screen->video); + int width = screen->pending_resize_width; + int height = screen->pending_resize_height; + screen->pending_resize_width = 0; + screen->pending_resize_height = 0; + // Ignorar tamaños inválidos + if (width < 1 || height < 1) { + LOGD("Ignoring invalid resize: %dx%d", width, height); + return 0; + } + LOGD("[RESIZE_FINISHED] Notifying display resize: %dx%d", width, height); + sc_screen_send_resize_display(screen, width, height); + return 0; +} diff --git a/app/src/screen.h b/app/src/screen.h index 6621b2d2..088bb4ab 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -59,7 +59,21 @@ struct sc_screen { // client orientation enum sc_orientation orientation; // rectangle of the content (excluding black borders) - struct SDL_Rect rect; + SDL_Rect rect; + + // --- Añadido para evitar bucles y zonas negras en displays redimensionables --- + int last_window_width; + int last_window_height; + int last_display_width; + int last_display_height; + // ----------------------------------------------------------------------------- + + // --- Añadido para resize diferido --- + int pending_resize_width; + int pending_resize_height; + SDL_TimerID resize_timer; + // ----------------------------------- + bool has_frame; bool fullscreen; bool maximized; @@ -69,6 +83,11 @@ struct sc_screen { bool paused; AVFrame *resume_frame; + + bool resizable_new_display; + bool initial_setup; // track if we're in initial window setup phase + bool content_driven_resize; // track if we're in content-driven resize (to avoid feedback loop) + uint64_t last_resize_time; // timestamp of last resize to implement debouncing }; struct sc_screen_params { @@ -100,6 +119,7 @@ struct sc_screen_params { bool fullscreen; bool start_fps_counter; + bool resizable_new_display; }; // initialize screen, create window, renderer and texture (window is hidden) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 66bb68e8..e04340d2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -589,10 +589,20 @@ public class Options { // - "x/" // - "x" // - "/" + // - "x/:r" + // - "x:r" + // - "/:r" + // - ":r" if (newDisplay.isEmpty()) { return new NewDisplay(); } + // Check for resizable flag + boolean resizable = newDisplay.endsWith(":r"); + if (resizable) { + newDisplay = newDisplay.substring(0, newDisplay.length() - 2); + } + String[] tokens = newDisplay.split("/"); Size size; @@ -612,7 +622,7 @@ public class Options { dpi = 0; } - return new NewDisplay(size, dpi); + return new NewDisplay(size, dpi, resizable); } private static Pair parseCaptureOrientation(String value) { 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..44484a88 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_RESIZE_DISPLAY = 18; public static final long SEQUENCE_INVALID = 0; @@ -53,6 +54,8 @@ public final class ControlMessage { private boolean on; private int vendorId; private int productId; + private int width; + private int height; private ControlMessage() { } @@ -166,6 +169,14 @@ public final class ControlMessage { return msg; } + public static ControlMessage createResizeDisplay(int width, int height) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_RESIZE_DISPLAY; + msg.width = width; + msg.height = height; + return msg; + } + public int getType() { return type; } @@ -249,4 +260,12 @@ public final class ControlMessage { public int getProductId() { return productId; } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } } 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..98cbc690 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_RESIZE_DISPLAY: + return parseResizeDisplay(); case ControlMessage.TYPE_UHID_CREATE: return parseUhidCreate(); case ControlMessage.TYPE_UHID_INPUT: @@ -166,6 +168,12 @@ public class ControlMessageReader { return ControlMessage.createStartApp(name); } + private ControlMessage parseResizeDisplay() throws IOException { + int width = dis.readUnsignedShort(); + int height = dis.readUnsignedShort(); + return ControlMessage.createResizeDisplay(width, height); + } + private Position parsePosition() throws IOException { int x = dis.readInt(); int y = dis.readInt(); 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..d86220ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -12,6 +12,7 @@ import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.video.NewDisplayCapture; import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.wrappers.ClipboardManager; @@ -331,6 +332,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_RESET_VIDEO: resetVideo(); break; + case ControlMessage.TYPE_RESIZE_DISPLAY: + resizeDisplay(msg.getWidth(), msg.getHeight()); + break; default: // do nothing } @@ -754,4 +758,18 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { surfaceCapture.requestInvalidate(); } } + + private void resizeDisplay(int width, int height) { + if (surfaceCapture instanceof NewDisplayCapture) { + NewDisplayCapture newDisplayCapture = (NewDisplayCapture) surfaceCapture; + if (newDisplayCapture.isResizable()) { + Ln.i("Resizing display to " + width + "x" + height); + newDisplayCapture.resizeDisplay(width, height); + } else { + Ln.w("Display is not resizable"); + } + } else { + Ln.w("Resize display not supported for current capture type"); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java index 3aa2996a..3f63c2ea 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java @@ -3,14 +3,23 @@ package com.genymobile.scrcpy.device; public final class NewDisplay { private Size size; private int dpi; + private boolean resizable; public NewDisplay() { - // Auto size and dpi + // Auto size and dpi, not resizable + this.resizable = false; } public NewDisplay(Size size, int dpi) { this.size = size; this.dpi = dpi; + this.resizable = false; + } + + public NewDisplay(Size size, int dpi, boolean resizable) { + this.size = size; + this.dpi = dpi; + this.resizable = resizable; } public Size getSize() { @@ -21,6 +30,10 @@ public final class NewDisplay { return dpi; } + public boolean isResizable() { + return resizable; + } + public boolean hasExplicitSize() { return size != null; } 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 792b3a8a..cb122699 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -63,6 +63,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size physicalSize; // the physical size of the display (without rotation) private int dpi; + private boolean resizable; public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { this.vdListener = vdListener; @@ -78,6 +79,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.angle = options.getAngle(); this.vdDestroyContent = options.getVDDestroyContent(); this.vdSystemDecorations = options.getVDSystemDecorations(); + this.resizable = newDisplay.isResizable(); } @Override @@ -197,7 +199,11 @@ public class NewDisplayCapture extends SurfaceCapture { ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); } - displaySizeMonitor.start(virtualDisplayId, this::invalidate); + // Only start display size monitoring for non-resizable displays + // For resizable displays, we control the resizing manually from the client + if (!resizable) { + displaySizeMonitor.start(virtualDisplayId, this::invalidate); + } } catch (Exception e) { Ln.e("Could not create display", e); throw new AssertionError("Could not create display"); @@ -264,4 +270,48 @@ public class NewDisplayCapture extends SurfaceCapture { public void requestInvalidate() { invalidate(); } + + public void resizeDisplay(int newWidth, int newHeight) { + if (!resizable || virtualDisplay == null) { + return; + } + + // Validate minimum size constraints + if (newWidth < 1 || newHeight < 1) { + Ln.w("Display size too small, ignoring resize: " + newWidth + "x" + newHeight); + return; + } + + try { + // Calculate new DPI based on the size change + int newDpi = dpi; + if (displaySize != null) { + // Scale DPI proportionally to maintain similar pixel density + int oldMax = Math.max(displaySize.getWidth(), displaySize.getHeight()); + int newMax = Math.max(newWidth, newHeight); + newDpi = (dpi * newMax) / oldMax; + } + + // Ensure DPI is within valid range (Android requires DPI >= 1) + newDpi = Math.max(1, newDpi); + + // Resize the virtual display + virtualDisplay.resize(newWidth, newHeight, newDpi); + + // Update our internal state + displaySize = new Size(newWidth, newHeight); + dpi = newDpi; + + Ln.i("Resized display to: " + newWidth + "x" + newHeight + "/" + newDpi); + + // Trigger a reconfiguration + invalidate(); + } catch (Exception e) { + Ln.e("Could not resize display", e); + } + } + + public boolean isResizable() { + return resizable; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 236a5f48..1aceb41d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -199,7 +199,17 @@ public class SurfaceEncoder implements AsyncProcessor { boolean eos; do { - int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); + int outputBufferId; + try { + // Use a finite timeout to handle MediaCodec reset scenarios gracefully + outputBufferId = codec.dequeueOutputBuffer(bufferInfo, 10000); // 10ms timeout + } catch (IllegalStateException e) { + // This can happen when MediaCodec is being reset (e.g., during display resize) + // The pending dequeue request gets cancelled + Ln.d("MediaCodec dequeue interrupted during reset: " + e.getMessage()); + break; // Exit the loop gracefully + } + try { eos = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; // On EOS, there might be data or not, depending on bufferInfo.size From ddaa33538a45a2b6dee070e284d43d8786e8a382 Mon Sep 17 00:00:00 2001 From: Reynaldo San Juan Date: Sat, 13 Sep 2025 15:52:09 +0200 Subject: [PATCH 2/8] clean up --- .../com/genymobile/scrcpy/control/ControlMessage.java | 1 - .../java/com/genymobile/scrcpy/device/NewDisplay.java | 8 +------- .../com/genymobile/scrcpy/video/NewDisplayCapture.java | 10 +++++----- 3 files changed, 6 insertions(+), 13 deletions(-) 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 44484a88..4c4e2b8f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -31,7 +31,6 @@ public final class ControlMessage { public static final int COPY_KEY_NONE = 0; public static final int COPY_KEY_COPY = 1; - public static final int COPY_KEY_CUT = 2; private int type; private String text; diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java index 3f63c2ea..34edccb3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java @@ -3,19 +3,13 @@ package com.genymobile.scrcpy.device; public final class NewDisplay { private Size size; private int dpi; - private boolean resizable; + private final boolean resizable; public NewDisplay() { // Auto size and dpi, not resizable this.resizable = false; } - public NewDisplay(Size size, int dpi) { - this.size = size; - this.dpi = dpi; - this.resizable = false; - } - public NewDisplay(Size size, int dpi, boolean resizable) { this.size = size; this.dpi = dpi; 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 cb122699..f3fa93d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -49,7 +49,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; - private int displayImePolicy; + private final int displayImePolicy; private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; @@ -63,7 +63,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size physicalSize; // the physical size of the display (without rotation) private int dpi; - private boolean resizable; + private final boolean resizable; public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { this.vdListener = vdListener; @@ -297,13 +297,13 @@ public class NewDisplayCapture extends SurfaceCapture { // Resize the virtual display virtualDisplay.resize(newWidth, newHeight, newDpi); - + // Update our internal state displaySize = new Size(newWidth, newHeight); dpi = newDpi; - + Ln.i("Resized display to: " + newWidth + "x" + newHeight + "/" + newDpi); - + // Trigger a reconfiguration invalidate(); } catch (Exception e) { From 3ef31a1f587f4ccf1819a337e54c277402b0f098 Mon Sep 17 00:00:00 2001 From: Reynaldo San Juan Date: Sat, 13 Sep 2025 19:16:10 +0200 Subject: [PATCH 3/8] feat: enhance resizable virtual display handling and improve resize quality --- app/src/input_manager.c | 5 ++ app/src/screen.c | 62 ++++++------------- .../genymobile/scrcpy/control/Controller.java | 1 + .../scrcpy/video/NewDisplayCapture.java | 13 ++-- 4 files changed, 34 insertions(+), 47 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3e4dd0f3..d6ab31ac 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -541,6 +541,11 @@ sc_input_manager_process_key(struct sc_input_manager *im, if (shift) { reset_video(im); } else { + // Disable MOD+R rotation in resizable virtual display mode + if (im->screen->resizable_new_display) { + LOGI("MOD+R rotation disabled in resizable virtual display mode"); + return; + } rotate_device(im); } } diff --git a/app/src/screen.c b/app/src/screen.c index 006cbc3c..520dc325 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -251,45 +251,8 @@ event_watcher(void *data, SDL_Event *event) { assert(screen->video); if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) { - int width = event->window.data1; - int height = event->window.data2; - if (screen->resizable_new_display) { - if ((!sizes_are_close(width, screen->last_window_width, 2) || !sizes_are_close(height, screen->last_window_height, 2)) && - !screen->initial_setup && !screen->content_driven_resize) { - screen->last_window_width = width; - screen->last_window_height = height; - // --- Resize diferido --- - screen->pending_resize_width = width; - screen->pending_resize_height = height; - if (screen->resize_timer) { - SDL_RemoveTimer(screen->resize_timer); - } - screen->resize_timer = SDL_AddTimer(RESIZE_FINISHED_DELAY, resize_timer_callback, screen); - // --- Fin resize diferido --- - sc_screen_render(screen, true); - } else if (screen->initial_setup) { - LOGD("[RESIZED] Initial setup resize ignored: %dx%d", width, height); - sc_screen_render(screen, true); - } else if (screen->content_driven_resize) { - LOGD("[RESIZED] Content-driven resize ignored: %dx%d", width, height); - sc_screen_render(screen, true); - } - } else { - // Modo normal - if (screen->content_driven_resize) { - LOGD("[RESIZED] Content-driven resize, solo render"); - sc_screen_render(screen, true); - return 0; - } - if (screen->initial_setup) { - LOGD("[RESIZED] Initial setup, solo render"); - sc_screen_render(screen, true); - return 0; - } - LOGD("[RESIZED] User resize detected: %dx%d", width, height); - sc_screen_render(screen, true); - sc_screen_send_resize_display(screen, width, height); - } + // Only handle rendering for the workaround, resize logic should be handled elsewhere + sc_screen_render(screen, true); } return 0; } @@ -924,8 +887,18 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { screen->last_window_width = w; screen->last_window_height = h; // --- Resize diferido --- - screen->pending_resize_width = w; - screen->pending_resize_height = h; + // Round to multiple of 8 to match server-side rounding and avoid quality degradation + int rounded_w = (w + 4) & ~7; // Round to nearest multiple of 8 + int rounded_h = (h + 4) & ~7; // Round to nearest multiple of 8 + + // Resize window to rounded dimensions to maintain quality + if (rounded_w != w || rounded_h != h) { + SDL_SetWindowSize(screen->window, rounded_w, rounded_h); + LOGD("Window resized to rounded dimensions: %dx%d (from %dx%d)", rounded_w, rounded_h, w, h); + } + + screen->pending_resize_width = rounded_w; + screen->pending_resize_height = rounded_h; if (screen->resize_timer) { SDL_RemoveTimer(screen->resize_timer); } @@ -1084,7 +1057,10 @@ static Uint32 resize_timer_callback(Uint32 interval, void *param) { LOGD("Ignoring invalid resize: %dx%d", width, height); return 0; } - LOGD("[RESIZE_FINISHED] Notifying display resize: %dx%d", width, height); - sc_screen_send_resize_display(screen, width, height); + // Round to multiple of 8 to match server-side rounding and avoid quality degradation + int rounded_w = (width + 4) & ~7; // Round to nearest multiple of 8 + int rounded_h = (height + 4) & ~7; // Round to nearest multiple of 8 + LOGD("[RESIZE_FINISHED] Notifying display resize: %dx%d (rounded from %dx%d)", rounded_w, rounded_h, width, height); + sc_screen_send_resize_display(screen, rounded_w, rounded_h); return 0; } 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 d86220ed..b7da5b42 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -313,6 +313,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { break; case ControlMessage.TYPE_ROTATE_DEVICE: Device.rotateDevice(getActionDisplayId()); + // The virtual display resize will be handled by the main display monitor break; case ControlMessage.TYPE_UHID_CREATE: getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData()); 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 f3fa93d4..c20fcf04 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -295,14 +295,19 @@ public class NewDisplayCapture extends SurfaceCapture { // Ensure DPI is within valid range (Android requires DPI >= 1) newDpi = Math.max(1, newDpi); - // Resize the virtual display - virtualDisplay.resize(newWidth, newHeight, newDpi); + // Round to multiple of 8 to avoid quality degradation from encoding constraints + Size roundedSize = new Size(newWidth, newHeight).round8(); + int roundedWidth = roundedSize.getWidth(); + int roundedHeight = roundedSize.getHeight(); + + // Resize the virtual display with rounded dimensions + virtualDisplay.resize(roundedWidth, roundedHeight, newDpi); // Update our internal state - displaySize = new Size(newWidth, newHeight); + displaySize = new Size(roundedWidth, roundedHeight); dpi = newDpi; - Ln.i("Resized display to: " + newWidth + "x" + newHeight + "/" + newDpi); + Ln.i("Resized display to: " + roundedWidth + "x" + roundedHeight + "/" + newDpi + " (rounded from " + newWidth + "x" + newHeight + ")"); // Trigger a reconfiguration invalidate(); From 62647ad7cd2e5f2ec69baad9c109932e069e0be6 Mon Sep 17 00:00:00 2001 From: Reynaldo San Juan Date: Sun, 14 Sep 2025 00:08:45 +0200 Subject: [PATCH 4/8] feat: add resolution factor support for resizable virtual displays --- app/src/cli.c | 11 ++++--- app/src/scrcpy.c | 26 +++++++++++++++- app/src/screen.c | 26 ++++++++++++++-- app/src/screen.h | 2 ++ doc/virtual_display.md | 11 +++++++ .../java/com/genymobile/scrcpy/Options.java | 30 +++++++++++++++---- .../genymobile/scrcpy/device/NewDisplay.java | 15 ++++++++++ 7 files changed, 108 insertions(+), 13 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index c5230521..07c8fd03 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -621,17 +621,20 @@ static const struct sc_option options[] = { { .longopt_id = OPT_NEW_DISPLAY, .longopt = "new-display", - .argdesc = "[x][/][:r]", + .argdesc = "[x][/][:r[]]", .optional_arg = true, .text = "Create a new display with the specified resolution and " "density. If not provided, they default to the main display " - "dimensions and DPI. Add ':r' to make the display resizable.\n" + "dimensions and DPI. Add ':r' to make the display resizable. " + "Optionally add a resolution factor after ':r' to scale the " + "display size (between 0.1 and 10.0).\n" "Examples:\n" " --new-display=1920x1080\n" " --new-display=1920x1080/420 # force 420 dpi\n" " --new-display=1920x1080/420:r # resizable display\n" - " --new-display=:r # resizable with main display size\n" - " --new-display=/420:r # resizable with main display size and 420 dpi\n" + " --new-display=1920x1080/420:r2.0 # resizable with 2x resolution\n" + " --new-display=:r0.5 # resizable with main display size and half resolution\n" + " --new-display=/420:r1.5 # resizable with main display size, 420 dpi and 1.5x resolution\n" " --new-display # main display size and density\n" " --new-display=/240 # main display size and 240 dpi", }, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 44bdcb3c..1aa93337 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -803,7 +803,30 @@ aoa_complete: options->window_title ? options->window_title : info->device_name; // Check if new_display is resizable (contains :r) - bool resizable_new_display = options->new_display && strstr(options->new_display, ":r") != NULL; + bool resizable_new_display = false; + float resolution_factor = 1.0f; + + if (options->new_display) { + const char *r_pos = strstr(options->new_display, ":r"); + if (r_pos) { + resizable_new_display = true; + + // Check if there's a resolution factor after ":r" + const char *factor_start = r_pos + 2; // Skip ":r" + if (*factor_start && (*factor_start >= '0' && *factor_start <= '9')) { + // Parse the resolution factor + char *end; + resolution_factor = strtof(factor_start, &end); + if (resolution_factor <= 0.1f) { + // Prevent too small factors + resolution_factor = 0.1f; + } else if (resolution_factor > 10.0f) { + // Prevent too large factors + resolution_factor = 10.0f; + } + } + } + } struct sc_screen_params screen_params = { .video = options->video_playback, @@ -828,6 +851,7 @@ aoa_complete: .fullscreen = options->fullscreen, .start_fps_counter = options->start_fps_counter, .resizable_new_display = resizable_new_display, + .resolution_factor = resolution_factor, }; if (!sc_screen_init(&s->screen, &screen_params)) { diff --git a/app/src/screen.c b/app/src/screen.c index 520dc325..35447a40 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -355,6 +355,9 @@ sc_screen_init(struct sc_screen *screen, screen->req.start_fps_counter = params->start_fps_counter; screen->resizable_new_display = params->resizable_new_display; + screen->resolution_factor = params->resolution_factor; + + LOGI("Resolution factor: %.2f", screen->resolution_factor); bool ok = sc_frame_buffer_init(&screen->fb); if (!ok) { @@ -957,14 +960,30 @@ sc_screen_send_resize_display(struct sc_screen *screen, int width, int height) { LOGD("Display size too small, ignoring resize: %dx%d", width, height); return; } + + // Apply resolution factor if in resizable mode + int adjusted_width = width; + int adjusted_height = height; + + if (screen->resizable_new_display && screen->resolution_factor != 1.0f) { + adjusted_width = (int)(width * screen->resolution_factor); + adjusted_height = (int)(height * screen->resolution_factor); + + // Ensure minimum size + if (adjusted_width < 1) adjusted_width = 1; + if (adjusted_height < 1) adjusted_height = 1; + + LOGD("Applying resolution factor %.2f: %dx%d -> %dx%d", + screen->resolution_factor, width, height, adjusted_width, adjusted_height); + } struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_RESIZE_DISPLAY; - msg.resize_display.width = (uint16_t) width; - msg.resize_display.height = (uint16_t) height; + msg.resize_display.width = (uint16_t) adjusted_width; + msg.resize_display.height = (uint16_t) adjusted_height; if (!sc_controller_push_msg(screen->im.controller, &msg)) { - LOGW("Could not request display resize to %dx%d", width, height); + LOGW("Could not request display resize to %dx%d", adjusted_width, adjusted_height); } } @@ -1060,6 +1079,7 @@ static Uint32 resize_timer_callback(Uint32 interval, void *param) { // Round to multiple of 8 to match server-side rounding and avoid quality degradation int rounded_w = (width + 4) & ~7; // Round to nearest multiple of 8 int rounded_h = (height + 4) & ~7; // Round to nearest multiple of 8 + LOGD("[RESIZE_FINISHED] Notifying display resize: %dx%d (rounded from %dx%d)", rounded_w, rounded_h, width, height); sc_screen_send_resize_display(screen, rounded_w, rounded_h); return 0; diff --git a/app/src/screen.h b/app/src/screen.h index 088bb4ab..c9a58309 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -85,6 +85,7 @@ struct sc_screen { AVFrame *resume_frame; bool resizable_new_display; + float resolution_factor; // Factor to multiply window size in resizable mode bool initial_setup; // track if we're in initial window setup phase bool content_driven_resize; // track if we're in content-driven resize (to avoid feedback loop) uint64_t last_resize_time; // timestamp of last resize to implement debouncing @@ -120,6 +121,7 @@ struct sc_screen_params { bool fullscreen; bool start_fps_counter; bool resizable_new_display; + float resolution_factor; // Factor to multiply window size in resizable mode }; // initialize screen, create window, renderer and texture (window is hidden) diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 9f962127..ffb8ef0f 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -11,6 +11,17 @@ scrcpy --new-display # use the main display size and density scrcpy --new-display=/240 # use the main display size and 240 dpi ``` +You can make the display resizable by adding `:r` to the option. Additionally, you can specify a resolution factor after `:r` to scale the display size: + +```bash +scrcpy --new-display=1920x1080:r # resizable display +scrcpy --new-display=1920x1080:r2.0 # resizable with 2x resolution +scrcpy --new-display=:r0.5 # resizable with half resolution +scrcpy --new-display=/420:r1.5 # resizable with 1.5x resolution +``` + +The resolution factor must be between 0.1 and 10.0. + The new virtual display is destroyed on exit. ## Start app diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index e04340d2..18b8859f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -593,14 +593,34 @@ public class Options { // - "x:r" // - "/:r" // - ":r" + // - "x/:r" + // - "x:r" + // - "/:r" + // - ":r" if (newDisplay.isEmpty()) { return new NewDisplay(); } - // Check for resizable flag - boolean resizable = newDisplay.endsWith(":r"); - if (resizable) { - newDisplay = newDisplay.substring(0, newDisplay.length() - 2); + // Check for resizable flag and resolution factor + boolean resizable = false; + float resolutionFactor = 1.0f; + int rIndex = newDisplay.indexOf(":r"); + if (rIndex >= 0) { + resizable = true; + String factorStr = newDisplay.substring(rIndex + 2); + if (!factorStr.isEmpty()) { + try { + resolutionFactor = Float.parseFloat(factorStr); + if (resolutionFactor <= 0.1f) { + resolutionFactor = 0.1f; + } else if (resolutionFactor > 10.0f) { + resolutionFactor = 10.0f; + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid resolution factor: " + factorStr); + } + } + newDisplay = newDisplay.substring(0, rIndex); } String[] tokens = newDisplay.split("/"); @@ -622,7 +642,7 @@ public class Options { dpi = 0; } - return new NewDisplay(size, dpi, resizable); + return new NewDisplay(size, dpi, resizable, resolutionFactor); } private static Pair parseCaptureOrientation(String value) { diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java index 34edccb3..7bdfab58 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java @@ -4,16 +4,23 @@ public final class NewDisplay { private Size size; private int dpi; private final boolean resizable; + private float resolutionFactor; public NewDisplay() { // Auto size and dpi, not resizable this.resizable = false; + this.resolutionFactor = 1.0f; } public NewDisplay(Size size, int dpi, boolean resizable) { + this(size, dpi, resizable, 1.0f); + } + + public NewDisplay(Size size, int dpi, boolean resizable, float resolutionFactor) { this.size = size; this.dpi = dpi; this.resizable = resizable; + this.resolutionFactor = resolutionFactor; } public Size getSize() { @@ -35,4 +42,12 @@ public final class NewDisplay { public boolean hasExplicitDpi() { return dpi != 0; } + + public float getResolutionFactor() { + return resolutionFactor; + } + + public boolean hasResolutionFactor() { + return resolutionFactor != 1.0f; + } } From 34037115a0be9fa9825f1a8b85b9271c41642d66 Mon Sep 17 00:00:00 2001 From: Reynaldo San Juan Date: Sun, 14 Sep 2025 00:17:09 +0200 Subject: [PATCH 5/8] feat: enforce maximum dimensions for resizable virtual displays to prevent encoder issues --- app/src/screen.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index 35447a40..c6dbaf86 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -973,6 +973,22 @@ sc_screen_send_resize_display(struct sc_screen *screen, int width, int height) { if (adjusted_width < 1) adjusted_width = 1; if (adjusted_height < 1) adjusted_height = 1; + // Ensure maximum size to avoid encoder issues + // Most Android devices support up to 4K resolution for encoding + const int MAX_DIMENSION = 2048; // Safe limit for most devices + if (adjusted_width > MAX_DIMENSION || adjusted_height > MAX_DIMENSION) { + // Scale down proportionally to fit within limits + float scale = 1.0f; + if (adjusted_width > adjusted_height) { + scale = (float)MAX_DIMENSION / adjusted_width; + } else { + scale = (float)MAX_DIMENSION / adjusted_height; + } + adjusted_width = (int)(adjusted_width * scale); + adjusted_height = (int)(adjusted_height * scale); + LOGW("Limiting dimensions to %dx%d to avoid encoder issues", adjusted_width, adjusted_height); + } + LOGD("Applying resolution factor %.2f: %dx%d -> %dx%d", screen->resolution_factor, width, height, adjusted_width, adjusted_height); } From 5d79f8d0dc6818eb76a9b43f055b8571270d9c28 Mon Sep 17 00:00:00 2001 From: Reynaldo San Juan Date: Sun, 14 Sep 2025 00:21:58 +0200 Subject: [PATCH 6/8] feat: remove resolution factor from NewDisplay and Options for simplification --- .../java/com/genymobile/scrcpy/Options.java | 22 ++++--------------- .../genymobile/scrcpy/device/NewDisplay.java | 16 +------------- 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 18b8859f..f30f520d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -1,5 +1,8 @@ package com.genymobile.scrcpy; +import android.graphics.Rect; +import android.util.Pair; + import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.device.Device; @@ -14,9 +17,6 @@ import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.wrappers.WindowManager; -import android.graphics.Rect; -import android.util.Pair; - import java.util.List; import java.util.Locale; @@ -603,23 +603,9 @@ public class Options { // Check for resizable flag and resolution factor boolean resizable = false; - float resolutionFactor = 1.0f; int rIndex = newDisplay.indexOf(":r"); if (rIndex >= 0) { resizable = true; - String factorStr = newDisplay.substring(rIndex + 2); - if (!factorStr.isEmpty()) { - try { - resolutionFactor = Float.parseFloat(factorStr); - if (resolutionFactor <= 0.1f) { - resolutionFactor = 0.1f; - } else if (resolutionFactor > 10.0f) { - resolutionFactor = 10.0f; - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid resolution factor: " + factorStr); - } - } newDisplay = newDisplay.substring(0, rIndex); } @@ -642,7 +628,7 @@ public class Options { dpi = 0; } - return new NewDisplay(size, dpi, resizable, resolutionFactor); + return new NewDisplay(size, dpi, resizable); } private static Pair parseCaptureOrientation(String value) { diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java index 7bdfab58..c5270a05 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java @@ -4,23 +4,16 @@ public final class NewDisplay { private Size size; private int dpi; private final boolean resizable; - private float resolutionFactor; public NewDisplay() { // Auto size and dpi, not resizable this.resizable = false; - this.resolutionFactor = 1.0f; } public NewDisplay(Size size, int dpi, boolean resizable) { - this(size, dpi, resizable, 1.0f); - } - - public NewDisplay(Size size, int dpi, boolean resizable, float resolutionFactor) { this.size = size; this.dpi = dpi; this.resizable = resizable; - this.resolutionFactor = resolutionFactor; } public Size getSize() { @@ -42,12 +35,5 @@ public final class NewDisplay { public boolean hasExplicitDpi() { return dpi != 0; } - - public float getResolutionFactor() { - return resolutionFactor; - } - - public boolean hasResolutionFactor() { - return resolutionFactor != 1.0f; - } + } From a2bd8b5531ec46d86fcda46d61c53d2875280fc3 Mon Sep 17 00:00:00 2001 From: Reynaldo San Juan Date: Sun, 14 Sep 2025 00:31:15 +0200 Subject: [PATCH 7/8] feat: add automatic resolution factor calculation for resizable virtual displays --- app/src/scrcpy.c | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1aa93337..2203cf52 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -805,6 +805,7 @@ aoa_complete: // Check if new_display is resizable (contains :r) bool resizable_new_display = false; float resolution_factor = 1.0f; + bool auto_resolution_factor = false; if (options->new_display) { const char *r_pos = strstr(options->new_display, ":r"); @@ -824,10 +825,49 @@ aoa_complete: // Prevent too large factors resolution_factor = 10.0f; } + } else { + // No explicit resolution factor provided, will calculate automatically + auto_resolution_factor = true; + LOGI("No resolution factor specified, will calculate automatically"); } } } + // Si se solicitó cálculo automático del factor de resolución, calcularlo basado en el tamaño de la pantalla principal + if (auto_resolution_factor && resizable_new_display) { + // Obtener el tamaño de la pantalla principal + SDL_Rect display_bounds; + if (SDL_GetDisplayUsableBounds(0, &display_bounds) == 0) { + // Calcular el factor de resolución basado en la relación entre el tamaño de la pantalla + // y el tamaño de la ventana solicitado + int display_width = display_bounds.w; + int display_height = display_bounds.h; + + // Usar el tamaño de ventana solicitado o un valor predeterminado + int window_width = options->window_width ? options->window_width : display_width / 2; + int window_height = options->window_height ? options->window_height : display_height / 2; + + // Calcular el factor como la relación entre el tamaño de la pantalla y el tamaño de la ventana + // Usar el menor de los factores para evitar que la ventana sea más grande que la pantalla + float width_factor = (float)display_width / window_width; + float height_factor = (float)display_height / window_height; + resolution_factor = width_factor < height_factor ? width_factor : height_factor; + + // Limitar el factor entre 0.1 y 10.0 + if (resolution_factor < 0.1f) { + resolution_factor = 0.1f; + } else if (resolution_factor > 10.0f) { + resolution_factor = 10.0f; + } + + LOGI("Calculated automatic resolution factor: %.2f (display: %dx%d, window: %dx%d)", + resolution_factor, display_width, display_height, window_width, window_height); + } else { + LOGW("Could not get display bounds, using default resolution factor: 1.0"); + resolution_factor = 1.0f; + } + } + struct sc_screen_params screen_params = { .video = options->video_playback, .controller = controller, From e568d07c3bda8f992456f1fa6787e48225df98aa Mon Sep 17 00:00:00 2001 From: Reynaldo San Juan Date: Sun, 14 Sep 2025 10:17:36 +0200 Subject: [PATCH 8/8] feat: update documentation and comments for resizable display resolution factors --- app/src/cli.c | 6 +++--- app/src/scrcpy.c | 22 ++++++---------------- doc/virtual_display.md | 6 +++--- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 07c8fd03..205e194f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -632,9 +632,9 @@ static const struct sc_option options[] = { " --new-display=1920x1080\n" " --new-display=1920x1080/420 # force 420 dpi\n" " --new-display=1920x1080/420:r # resizable display\n" - " --new-display=1920x1080/420:r2.0 # resizable with 2x resolution\n" - " --new-display=:r0.5 # resizable with main display size and half resolution\n" - " --new-display=/420:r1.5 # resizable with main display size, 420 dpi and 1.5x resolution\n" + " --new-display=1920x1080/420:r2.0 # resizable with 2x factor\n" + " --new-display=:r0.5 # resizable with main display size and 0.5x factor\n" + " --new-display=/420:r1.5 # resizable with main display size, 420 dpi and 1.5x factor\n" " --new-display # main display size and density\n" " --new-display=/240 # main display size and 240 dpi", }, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2203cf52..2823f71f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -806,12 +806,11 @@ aoa_complete: bool resizable_new_display = false; float resolution_factor = 1.0f; bool auto_resolution_factor = false; - + if (options->new_display) { const char *r_pos = strstr(options->new_display, ":r"); if (r_pos) { resizable_new_display = true; - // Check if there's a resolution factor after ":r" const char *factor_start = r_pos + 2; // Skip ":r" if (*factor_start && (*factor_start >= '0' && *factor_start <= '9')) { @@ -833,41 +832,32 @@ aoa_complete: } } - // Si se solicitó cálculo automático del factor de resolución, calcularlo basado en el tamaño de la pantalla principal + // If automatic resolution factor calculation was requested, calculate it based on the main display size if (auto_resolution_factor && resizable_new_display) { - // Obtener el tamaño de la pantalla principal + // Get the main display size SDL_Rect display_bounds; if (SDL_GetDisplayUsableBounds(0, &display_bounds) == 0) { - // Calcular el factor de resolución basado en la relación entre el tamaño de la pantalla - // y el tamaño de la ventana solicitado + // Calculate the resolution factor based on the ratio between the display size and the requested window size int display_width = display_bounds.w; int display_height = display_bounds.h; - - // Usar el tamaño de ventana solicitado o un valor predeterminado int window_width = options->window_width ? options->window_width : display_width / 2; int window_height = options->window_height ? options->window_height : display_height / 2; - - // Calcular el factor como la relación entre el tamaño de la pantalla y el tamaño de la ventana - // Usar el menor de los factores para evitar que la ventana sea más grande que la pantalla float width_factor = (float)display_width / window_width; float height_factor = (float)display_height / window_height; resolution_factor = width_factor < height_factor ? width_factor : height_factor; - - // Limitar el factor entre 0.1 y 10.0 if (resolution_factor < 0.1f) { resolution_factor = 0.1f; } else if (resolution_factor > 10.0f) { resolution_factor = 10.0f; } - - LOGI("Calculated automatic resolution factor: %.2f (display: %dx%d, window: %dx%d)", + LOGI("Calculated automatic resolution factor: %.2f (display: %dx%d, window: %dx%d)", resolution_factor, display_width, display_height, window_width, window_height); } else { LOGW("Could not get display bounds, using default resolution factor: 1.0"); resolution_factor = 1.0f; } } - + struct sc_screen_params screen_params = { .video = options->video_playback, .controller = controller, diff --git a/doc/virtual_display.md b/doc/virtual_display.md index ffb8ef0f..1e2f7bb0 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -15,9 +15,9 @@ You can make the display resizable by adding `:r` to the option. Additionally, y ```bash scrcpy --new-display=1920x1080:r # resizable display -scrcpy --new-display=1920x1080:r2.0 # resizable with 2x resolution -scrcpy --new-display=:r0.5 # resizable with half resolution -scrcpy --new-display=/420:r1.5 # resizable with 1.5x resolution +scrcpy --new-display=1920x1080:r2.0 # resizable with 2x factor +scrcpy --new-display=:r0.5 # resizable with 0.5x factor +scrcpy --new-display=/420:r1.5 # resizable with 1.5x factor ``` The resolution factor must be between 0.1 and 10.0.