[#663] feat: resizable virtual display

This commit is contained in:
Reynaldo San Juan 2025-09-13 15:45:45 +02:00
parent f663bbec12
commit 526e4a81ed
13 changed files with 323 additions and 13 deletions

View file

@ -621,14 +621,17 @@ static const struct sc_option options[] = {
{
.longopt_id = OPT_NEW_DISPLAY,
.longopt = "new-display",
.argdesc = "[<width>x<height>][/<dpi>]",
.argdesc = "[<width>x<height>][/<dpi>][: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",
},

View file

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

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_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;
};
};

View file

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

View file

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

View file

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

View file

@ -589,10 +589,20 @@ public class Options {
// - "<width>x<height>/<dpi>"
// - "<width>x<height>"
// - "/<dpi>"
// - "<width>x<height>/<dpi>:r"
// - "<width>x<height>:r"
// - "/<dpi>: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<Orientation.Lock, Orientation> parseCaptureOrientation(String value) {

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_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;
}
}

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_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();

View file

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

View file

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

View file

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

View file

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