diff --git a/app/src/cli.c b/app/src/cli.c index f7d7e390af..32a579e876 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -93,6 +93,10 @@ enum { OPT_DISPLAY_ORIENTATION, OPT_RECORD_ORIENTATION, OPT_ORIENTATION, + OPT_ROTATION_OFFSET, + OPT_SCALE, + OPT_POSITION_X_OFFSET, + OPT_POSITION_Y_OFFSET, }; struct sc_option { @@ -837,6 +841,36 @@ static const struct sc_option options[] = { .text = "Set the initial window height.\n" "Default is 0 (automatic).", }, + { + .longopt_id = OPT_ROTATION_OFFSET, + .longopt = "rotation-offset", + .argdesc = "value", + .text = "Set the display rotation offset in degrees.\n" + "Positive values rotate clockwise, negative values " + "rotate counter-clockwise.\n" + "Default is 0 (automatic).", + }, + { + .longopt_id = OPT_SCALE, + .longopt = "scale", + .argdesc = "value", + .text = "Set the display scale in integer percentage.\n" + "Default is 100 (automatic).", + }, + { + .longopt_id = OPT_POSITION_X_OFFSET, + .longopt = "position-x-offset", + .argdesc = "value", + .text = "Set the display horizontal position offset.\n" + "Default is 0 (automatic).", + }, + { + .longopt_id = OPT_POSITION_Y_OFFSET, + .longopt = "position-y-offset", + .argdesc = "value", + .text = "Set the display vertical position offset.\n" + "Default is 0 (automatic).", + }, }; static const struct sc_shortcut shortcuts[] = { @@ -1937,6 +1971,32 @@ parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { } +static bool +parse_rotation_offset(const char *s, int16_t *rotation_offset) { + long value; + bool ok = parse_integer_arg(s, &value, false, -360, 360, + "display rotation offset"); + if (!ok) { + return false; + } + + *rotation_offset = (int16_t) value; + return true; +} + +static bool +parse_scale(const char *s, uint16_t *scale) { + long value; + bool ok = parse_integer_arg(s, &value, false, 1, 1000, + "display scale"); + if (!ok) { + return false; + } + + *scale = (uint16_t) value; + return true; +} + static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { @@ -2359,6 +2419,26 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_CAMERA_HIGH_SPEED: opts->camera_high_speed = true; break; + case OPT_ROTATION_OFFSET: + if (!parse_rotation_offset(optarg, &opts->rotation_offset)) { + return false; + } + break; + case OPT_SCALE: + if (!parse_scale(optarg, &opts->scale)) { + return false; + } + break; + case OPT_POSITION_X_OFFSET: + if (!parse_window_position(optarg, &opts->position_x_offset)) { + return false; + } + break; + case OPT_POSITION_Y_OFFSET: + if (!parse_window_position(optarg, &opts->position_y_offset)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/coords.h b/app/src/coords.h index cdabb782c2..30fb1b3025 100644 --- a/app/src/coords.h +++ b/app/src/coords.h @@ -21,4 +21,10 @@ struct sc_position { struct sc_point point; }; +struct sc_transform { + int16_t rotation; + uint16_t scale; + struct sc_point position; +}; + #endif diff --git a/app/src/display.c b/app/src/display.c index 906b5d657b..8fda7f0074 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -234,7 +234,7 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - enum sc_orientation orientation) { + enum sc_orientation orientation, struct sc_transform *transform_offsets) { SDL_RenderClear(display->renderer); if (display->pending.flags) { @@ -247,37 +247,46 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, SDL_Renderer *renderer = display->renderer; SDL_Texture *texture = display->texture; - if (orientation == SC_ORIENTATION_0) { - int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); - if (ret) { - LOGE("Could not render texture: %s", SDL_GetError()); - return SC_DISPLAY_RESULT_ERROR; - } + int16_t rotation_offset = transform_offsets->rotation; + if (rotation_offset < 0) { + rotation_offset = rotation_offset + 360; + } + + unsigned cw_rotation = sc_orientation_get_rotation(orientation); + double angle = (90 * cw_rotation) + rotation_offset; + + const SDL_Rect *dstrect = NULL; + SDL_Rect rect; + + rect.x = geometry->x + transform_offsets->position.x; + rect.y = geometry->y + transform_offsets->position.y; + + if (transform_offsets->scale != 100) { + rect.w = geometry->w * transform_offsets->scale / 100; + rect.h = geometry->h * transform_offsets->scale / 100; } else { - unsigned cw_rotation = sc_orientation_get_rotation(orientation); - double angle = 90 * cw_rotation; - - const SDL_Rect *dstrect = NULL; - SDL_Rect rect; - if (sc_orientation_is_swap(orientation)) { - rect.x = geometry->x + (geometry->w - geometry->h) / 2; - rect.y = geometry->y + (geometry->h - geometry->w) / 2; - rect.w = geometry->h; - rect.h = geometry->w; - dstrect = ▭ - } else { - dstrect = geometry; - } + rect.w = geometry->w; + rect.h = geometry->h; + } - SDL_RendererFlip flip = sc_orientation_is_mirror(orientation) - ? SDL_FLIP_HORIZONTAL : 0; + if (sc_orientation_is_swap(orientation)) { + rect.x = rect.x + (rect.w - rect.h) / 2; + rect.y = rect.y + (rect.h - rect.w) / 2; + int width = rect.w; + rect.w = rect.h; + rect.h = width; + } - int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, - NULL, flip); - if (ret) { - LOGE("Could not render texture: %s", SDL_GetError()); - return SC_DISPLAY_RESULT_ERROR; - } + dstrect = ▭ + + SDL_RendererFlip flip = sc_orientation_is_mirror(orientation) + ? SDL_FLIP_HORIZONTAL : 0; + + int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, + NULL, flip); + if (ret) { + LOGE("Could not render texture: %s", SDL_GetError()); + return SC_DISPLAY_RESULT_ERROR; } SDL_RenderPresent(display->renderer); diff --git a/app/src/display.h b/app/src/display.h index 643ce73c63..2d8fde371e 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -55,6 +55,6 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame); enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - enum sc_orientation orientation); + enum sc_orientation orientation, struct sc_transform *transform_offsets); #endif diff --git a/app/src/options.c b/app/src/options.c index a13df585a6..762f309dd4 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -41,6 +41,10 @@ const struct scrcpy_options scrcpy_options_default = { .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0, + .rotation_offset = 0, + .scale = 100, + .position_x_offset = 0, + .position_y_offset = 0, .window_x = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED, .window_width = 0, diff --git a/app/src/options.h b/app/src/options.h index 11e64fa19e..abcbdeb3d2 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -221,6 +221,10 @@ struct scrcpy_options { enum sc_lock_video_orientation lock_video_orientation; enum sc_orientation display_orientation; enum sc_orientation record_orientation; + int16_t rotation_offset; + uint16_t scale; + int16_t position_x_offset; + int16_t position_y_offset; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index cf2e7e4786..f05e9384ee 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -702,6 +702,10 @@ scrcpy(struct scrcpy_options *options) { .window_height = options->window_height, .window_borderless = options->window_borderless, .orientation = options->display_orientation, + .rotation_offset = options->rotation_offset, + .scale = options->scale, + .position_x_offset = options->position_x_offset, + .position_y_offset = options->position_y_offset, .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, .start_fps_counter = options->start_fps_counter, diff --git a/app/src/screen.c b/app/src/screen.c index 091001bcbf..1174b9f86f 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -3,6 +3,7 @@ #include #include #include +#include #include "events.h" #include "icon.h" @@ -251,7 +252,7 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { } enum sc_display_result res = - sc_display_render(&screen->display, &screen->rect, screen->orientation); + sc_display_render(&screen->display, &screen->rect, screen->orientation, &screen->transform_offsets); (void) res; // any error already logged } @@ -380,6 +381,10 @@ sc_screen_init(struct sc_screen *screen, } screen->orientation = params->orientation; + screen->transform_offsets.rotation = params->rotation_offset; + screen->transform_offsets.scale = params->scale; + screen->transform_offsets.position.x = params->position_x_offset; + screen->transform_offsets.position.y = params->position_y_offset; if (screen->orientation != SC_ORIENTATION_0) { LOGI("Initial display orientation set to %s", sc_orientation_get_name(screen->orientation)); @@ -841,6 +846,24 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { return true; } +static void +sc_rotate_point(struct sc_point *point, + struct sc_point *pivot, + int16_t angle_in_degrees) { + const double deg_to_rad = M_PI / 180.0; + float32_t angle_in_radians = (float32_t)angle_in_degrees * deg_to_rad; + float32_t cosine = (float32_t)cos(angle_in_radians); + float32_t sine = (float32_t)sin(angle_in_radians); + + int32_t x = point->x; + int32_t y = point->y; + int32_t pivot_x = pivot->x; + int32_t pivot_y = pivot->y; + + point->x = (int32_t)(((x - pivot_x) * cosine) + ((y - pivot_y) * -sine) + pivot_x); + point->y = (int32_t)(((x - pivot_x) * sine) + ((y - pivot_y) * cosine) + pivot_y); +} + struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y) { @@ -848,50 +871,78 @@ sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; + int32_t w_half = w >> 1; + int32_t h_half = h >> 1; + struct sc_point pivot = { + .x = w_half, + .y = h_half, + }; + int8_t flip_factor = -1; + float32_t scale_factor = 100.0 / screen->transform_offsets.scale; // screen->rect must be initialized to avoid a division by zero assert(screen->rect.w && screen->rect.h); - x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; - y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; + float32_t w_factor = w / (float32_t)screen->rect.w; + float32_t h_factor = h / (float32_t)screen->rect.h; + x = (int64_t) (x - screen->rect.x) * w_factor; + y = (int64_t) (y - screen->rect.y) * h_factor; + int32_t x_offset = screen->transform_offsets.position.x * w_factor; + int32_t y_offset = screen->transform_offsets.position.y * h_factor; struct sc_point result; switch (orientation) { case SC_ORIENTATION_0: - result.x = x; - result.y = y; + result.x = (x - x_offset) * scale_factor; + result.y = (y - y_offset) * scale_factor; break; case SC_ORIENTATION_90: - result.x = y; - result.y = w - x; + result.x = (y - y_offset) * scale_factor; + result.y = w + ((x_offset - x) * scale_factor); + pivot.x = h_half; + pivot.y = w_half; break; case SC_ORIENTATION_180: - result.x = w - x; - result.y = h - y; + result.x = w + ((x_offset - x) * scale_factor); + result.y = h + ((y_offset - y) * scale_factor); break; case SC_ORIENTATION_270: - result.x = h - y; - result.y = x; + result.x = h + ((y_offset - y) * scale_factor); + result.y = (x - x_offset) * scale_factor; + pivot.x = h_half; + pivot.y = w_half; break; case SC_ORIENTATION_FLIP_0: - result.x = w - x; - result.y = y; + result.x = w + ((x_offset - x) * scale_factor); + result.y = (y - y_offset) * scale_factor; + flip_factor = 1; break; case SC_ORIENTATION_FLIP_90: - result.x = h - y; - result.y = w - x; + result.x = h + ((y_offset - y) * scale_factor); + result.y = w + ((x_offset - x) * scale_factor); + pivot.x = h_half; + pivot.y = w_half; + flip_factor = 1; break; case SC_ORIENTATION_FLIP_180: - result.x = x; - result.y = h - y; + result.x = (x - x_offset) * scale_factor; + result.y = h + ((y_offset - y) * scale_factor); + flip_factor = 1; break; default: assert(orientation == SC_ORIENTATION_FLIP_270); - result.x = y; - result.y = x; + result.x = (y - y_offset) * scale_factor; + result.y = (x - x_offset) * scale_factor; + pivot.x = h_half; + pivot.y = w_half; + flip_factor = 1; break; } + if (screen->transform_offsets.rotation != 0) { + sc_rotate_point(&result, &pivot, flip_factor * screen->transform_offsets.rotation); + } + return result; } diff --git a/app/src/screen.h b/app/src/screen.h index 46591be5cc..b604ae4856 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -64,6 +64,8 @@ struct sc_screen { SDL_Keycode mouse_capture_key_pressed; AVFrame *frame; + + struct sc_transform transform_offsets; }; struct sc_screen_params { @@ -92,6 +94,11 @@ struct sc_screen_params { bool fullscreen; bool start_fps_counter; + + int16_t rotation_offset; + uint16_t scale; + int16_t position_x_offset; + int16_t position_y_offset; }; // initialize screen, create window, renderer and texture (window is hidden) diff --git a/doc/video.md b/doc/video.md index ed92cb221b..08947fe198 100644 --- a/doc/video.md +++ b/doc/video.md @@ -141,6 +141,48 @@ to the MP4 or MKV target file. Flipping is not supported, so only the 4 first values are allowed when recording. +## Rotation offset + +Rotation offset in degrees to be applied (positive values rotate clockwise, +negative values rotate counter-clockwise): + +```bash +scrcpy --rotation-offset=45 +``` + +This is useful for Meta Quest 3 tilted screen setup: + +```bash +scrcpy --crop=2064:2208:0:0 --rotation-offset=22 +``` + +This is not supported when recording. + + +## Scale + +Scale in integer percentage to be applied (default is 100): + +```bash +scrcpy --scale=150 # 150% scale +``` + +This is not supported when recording. + + +## Position offset + +Position offset to be applied both horizontally and/or vertically +(default is 0 for both): + +```bash +scrcpy --position-x-offset=150 +scrcpy --position-y-offset=150 +``` + +This is not supported when recording. + + ## Crop The device screen may be cropped to mirror only part of the screen.