Add resizable virtual display feature

Introduce `--flex-display` to continuously resize the virtual display
to match the window.
This commit is contained in:
Romain Vimont 2026-04-17 08:30:25 +02:00
parent 00c941ab03
commit 3ca88616d2
18 changed files with 219 additions and 19 deletions

View file

@ -791,7 +791,8 @@ static const struct sc_option options[] = {
"and bottom or at the sides if needed).\n"
"\"disabled\": render the display at the top-left corner, "
"without scaling.\n"
"Default is \"letterbox\".",
"Default is \"letterbox\", unless --flex-display is set, in "
"which case it is \"disabled\".",
},
{
.longopt_id = OPT_REQUIRE_AUDIO,
@ -1016,6 +1017,11 @@ static const struct sc_option options[] = {
.text = "Set the initial window height.\n"
"Default is 0 (automatic).",
},
{
.shortopt = 'x',
.longopt = "flex-display",
.text = "Continuously resize the virtual display to match the window.",
},
};
static const struct sc_shortcut shortcuts[] = {
@ -2792,6 +2798,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case 'x':
opts->flex_display = true;
break;
default:
// getopt prints the error message on stderr
return false;
@ -2971,6 +2980,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
if (opts->render_fit == SC_RENDER_FIT_AUTO) {
opts->render_fit = opts->flex_display ? SC_RENDER_FIT_DISABLED
: SC_RENDER_FIT_LETTERBOX;
}
if (otg) {
if (!opts->control) {
LOGE("--no-control is not allowed in OTG mode");
@ -3090,6 +3104,28 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->flex_display) {
if (opts->video_source != SC_VIDEO_SOURCE_DISPLAY
|| !opts->new_display) {
LOGE("-x/--flex-display can only be applied to displays created "
"with --new-display");
return false;
}
if (opts->max_size) {
LOGE("--max-size is not compatible with -x/--flex-display");
return false;
}
if (opts->crop) {
LOGE("--crop is not compatible with -x/--flex-display");
return false;
}
// Force free resizing
opts->window_aspect_ratio_lock = false;
}
if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED
&& opts->display_id == 0 && !opts->new_display) {
LOGE("--display-ime-policy is only supported on a secondary display");

View file

@ -59,7 +59,7 @@ const struct scrcpy_options scrcpy_options_default = {
.display_orientation = SC_ORIENTATION_0,
.record_orientation = SC_ORIENTATION_0,
.display_ime_policy = SC_DISPLAY_IME_POLICY_UNDEFINED,
.render_fit = SC_RENDER_FIT_LETTERBOX,
.render_fit = SC_RENDER_FIT_AUTO,
.window_x = SC_WINDOW_POSITION_UNDEFINED,
.window_y = SC_WINDOW_POSITION_UNDEFINED,
.window_width = 0,
@ -117,6 +117,7 @@ const struct scrcpy_options scrcpy_options_default = {
.vd_destroy_content = true,
.vd_system_decorations = true,
.camera_torch = false,
.flex_display = false,
};
enum sc_orientation

View file

@ -221,6 +221,7 @@ enum sc_shortcut_mod {
};
enum sc_render_fit {
SC_RENDER_FIT_AUTO,
SC_RENDER_FIT_LETTERBOX,
SC_RENDER_FIT_DISABLED,
};
@ -336,6 +337,7 @@ struct scrcpy_options {
bool vd_destroy_content;
bool vd_system_decorations;
bool camera_torch;
bool flex_display;
};
extern const struct scrcpy_options scrcpy_options_default;

View file

@ -465,6 +465,7 @@ scrcpy(struct scrcpy_options *options) {
.camera_zoom = options->camera_zoom,
.vd_destroy_content = options->vd_destroy_content,
.vd_system_decorations = options->vd_system_decorations,
.flex_display = options->flex_display,
.list = options->list,
};
@ -797,6 +798,7 @@ aoa_complete:
struct sc_screen_params screen_params = {
.video = options->video_playback,
.camera = options->video_source == SC_VIDEO_SOURCE_CAMERA,
.flex_display = options->flex_display,
.controller = controller,
.fp = fp,
.kp = kp,

View file

@ -286,10 +286,27 @@ end:
}
static void
sc_screen_on_resize(struct sc_screen *screen) {
sc_screen_on_resize(struct sc_screen *screen, const SDL_WindowEvent *event) {
// This event can be triggered before the window is shown
if (screen->window_shown) {
sc_screen_render(screen, true);
if (screen->flex_display) {
assert(!screen->camera);
assert(!(event->data1 & !0xFFFF));
assert(!(event->data2 & !0xFFFF));
uint16_t width = event->data1;
uint16_t height = event->data2;
if (sc_orientation_is_swap(screen->orientation)) {
uint16_t tmp = width;
width = height;
height = tmp;
}
LOGV("resize_display(%" PRIu16 ", %" PRIu16 ")", width, height);
sc_controller_resize_display(screen->controller, width,
height);
}
}
}
@ -311,7 +328,7 @@ event_watcher(void *data, SDL_Event *event) {
if (event->type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) {
// In practice, it seems to always be called from the same thread in
// that specific case. Anyway, it's just a workaround.
sc_screen_on_resize(screen);
sc_screen_on_resize(screen, &event->window);
}
return true;
@ -406,6 +423,8 @@ sc_screen_frame_sink_push_session(struct sc_frame_sink *sink,
bool
sc_screen_init(struct sc_screen *screen,
const struct sc_screen_params *params) {
screen->controller = params->controller;
screen->resize_pending = false;
screen->window_shown = false;
screen->paused = false;
@ -418,6 +437,7 @@ sc_screen_init(struct sc_screen *screen,
screen->camera = params->camera;
screen->window_aspect_ratio_lock = params->window_aspect_ratio_lock;
screen->render_fit = params->render_fit;
screen->flex_display = params->flex_display;
screen->req.x = params->window_x;
screen->req.y = params->window_y;
@ -739,11 +759,13 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
assert(screen->video);
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
struct sc_size target_size = {
.width = (uint32_t) window_size.width * new_content_size.width
/ old_content_size.width,
.height = (uint32_t) window_size.height * new_content_size.height
/ old_content_size.height,
struct sc_size target_size = new_content_size;
if (!screen->flex_display) {
// Scale proportionally
target_size.width = (uint32_t) window_size.width * target_size.width
/ old_content_size.width;
target_size.height = (uint32_t) window_size.height * target_size.height
/ old_content_size.height;
};
target_size = get_optimal_size(target_size, new_content_size, true);
assert(is_windowed(screen));
@ -999,7 +1021,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
// If defined, then the actions are already performed by the event watcher
#ifndef CONTINUOUS_RESIZING_WORKAROUND
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
sc_screen_on_resize(screen);
sc_screen_on_resize(screen, &event->window);
return;
#endif
case SDL_EVENT_WINDOW_RESTORED:

View file

@ -37,6 +37,9 @@ struct sc_screen {
bool video;
bool camera;
bool window_aspect_ratio_lock;
bool flex_display;
struct sc_controller *controller;
struct sc_texture tex;
struct sc_input_manager im;
@ -96,6 +99,7 @@ struct sc_screen {
struct sc_screen_params {
bool video;
bool camera;
bool flex_display;
struct sc_controller *controller;
struct sc_file_pusher *fp;

View file

@ -413,6 +413,9 @@ execute_server(struct sc_server *server,
VALIDATE_STRING(params->new_display);
ADD_PARAM("new_display=%s", params->new_display);
}
if (params->flex_display) {
ADD_PARAM("flex_display=true");
}
if (params->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) {
ADD_PARAM("display_ime_policy=%s",
sc_server_get_display_ime_policy_name(params->display_ime_policy));

View file

@ -72,6 +72,7 @@ struct sc_server_params {
bool camera_torch;
bool vd_destroy_content;
bool vd_system_decorations;
bool flex_display;
uint8_t list;
};