scrcpy/app/src/screen.c
Romain Vimont f409eed4ff Add resizable virtual display feature
Introduce `--flex-display` to continuously resize the virtual display
to match the window.
2026-04-19 14:02:40 +02:00

1210 lines
38 KiB
C

#include "screen.h"
#include <assert.h>
#include <string.h>
#include <SDL3/SDL.h>
#include "events.h"
#include "icon.h"
#include "options.h"
#include "util/log.h"
#include "util/sdl.h"
#define DISPLAY_MARGINS 96
#define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink)
static void
set_aspect_ratio(struct sc_screen *screen, struct sc_size content_size) {
assert(content_size.width && content_size.height);
if (screen->window_aspect_ratio_lock) {
float ar = (float) content_size.width / content_size.height;
bool ok = SDL_SetWindowAspectRatio(screen->window, ar, ar);
if (!ok) {
LOGW("Could not set window aspect ratio: %s", SDL_GetError());
}
}
}
static inline struct sc_size
get_oriented_size(struct sc_size size, enum sc_orientation orientation) {
struct sc_size oriented_size;
if (sc_orientation_is_swap(orientation)) {
oriented_size.width = size.height;
oriented_size.height = size.width;
} else {
oriented_size.width = size.width;
oriented_size.height = size.height;
}
return oriented_size;
}
static inline bool
is_windowed(struct sc_screen *screen) {
return !(SDL_GetWindowFlags(screen->window) & (SDL_WINDOW_FULLSCREEN
| SDL_WINDOW_MINIMIZED
| SDL_WINDOW_MAXIMIZED));
}
// get the preferred display bounds (i.e. the screen bounds with some margins)
static bool
get_preferred_display_bounds(struct sc_size *bounds) {
SDL_Rect rect;
SDL_DisplayID display = SDL_GetPrimaryDisplay();
if (!display) {
LOGW("Could not get primary display: %s", SDL_GetError());
return false;
}
bool ok = SDL_GetDisplayUsableBounds(display, &rect);
if (!ok) {
LOGW("Could not get display usable bounds: %s", SDL_GetError());
return false;
}
bounds->width = MAX(0, rect.w - DISPLAY_MARGINS);
bounds->height = MAX(0, rect.h - DISPLAY_MARGINS);
return true;
}
static bool
is_optimal_size(struct sc_size current_size, struct sc_size content_size) {
// The size is optimal if we can recompute one dimension of the current
// size from the other
return current_size.height == current_size.width * content_size.height
/ content_size.width
|| current_size.width == current_size.height * content_size.width
/ content_size.height;
}
// return the optimal size of the window, with the following constraints:
// - it attempts to keep at least one dimension of the current_size (i.e. it
// crops the black borders)
// - it keeps the aspect ratio
// - it scales down to make it fit in the display_size
static struct sc_size
get_optimal_size(struct sc_size current_size, struct sc_size content_size,
bool within_display_bounds) {
if (content_size.width == 0 || content_size.height == 0) {
// avoid division by 0
return current_size;
}
struct sc_size window_size;
struct sc_size display_size;
if (!within_display_bounds ||
!get_preferred_display_bounds(&display_size)) {
// do not constraint the size
window_size = current_size;
} else {
window_size.width = MIN(current_size.width, display_size.width);
window_size.height = MIN(current_size.height, display_size.height);
}
if (is_optimal_size(window_size, content_size)) {
return window_size;
}
bool keep_width = content_size.width * window_size.height
> content_size.height * window_size.width;
if (keep_width) {
// remove black borders on top and bottom
window_size.height = content_size.height * window_size.width
/ content_size.width;
} else {
// remove black borders on left and right (or none at all if it already
// fits)
window_size.width = content_size.width * window_size.height
/ content_size.height;
}
return window_size;
}
// initially, there is no current size, so use the frame size as current size
// req_width and req_height, if not 0, are the sizes requested by the user
static inline struct sc_size
get_initial_optimal_size(struct sc_size content_size, uint16_t req_width,
uint16_t req_height) {
struct sc_size window_size;
if (!req_width && !req_height) {
window_size = get_optimal_size(content_size, content_size, true);
} else {
if (req_width) {
window_size.width = req_width;
} else {
// compute from the requested height
window_size.width = (uint32_t) req_height * content_size.width
/ content_size.height;
}
if (req_height) {
window_size.height = req_height;
} else {
// compute from the requested width
window_size.height = (uint32_t) req_width * content_size.height
/ content_size.width;
}
}
return window_size;
}
static inline bool
sc_screen_is_relative_mode(struct sc_screen *screen) {
// screen->im.mp may be NULL if --no-control
return screen->im.mp && screen->im.mp->relative_mode;
}
static void
compute_content_rect(struct sc_size render_size, struct sc_size content_size,
bool can_upscale, enum sc_render_fit render_fit,
SDL_FRect *rect) {
if (render_fit == SC_RENDER_FIT_DISABLED) {
rect->x = 0;
rect->y = 0;
rect->w = content_size.width;
rect->h = content_size.height;
return;
}
if (is_optimal_size(render_size, content_size)) {
rect->x = 0;
rect->y = 0;
rect->w = render_size.width;
rect->h = render_size.height;
return;
}
if (!can_upscale && content_size.width <= render_size.width
&& content_size.height <= render_size.height) {
// Center without upscaling
rect->x = (render_size.width - content_size.width) / 2.f;
rect->y = (render_size.height - content_size.height) / 2.f;
rect->w = content_size.width;
rect->h = content_size.height;
return;
}
bool keep_width = content_size.width * render_size.height
> content_size.height * render_size.width;
if (keep_width) {
rect->x = 0;
rect->w = render_size.width;
rect->h = (float) render_size.width * content_size.height
/ content_size.width;
rect->y = (render_size.height - rect->h) / 2.f;
} else {
rect->y = 0;
rect->h = render_size.height;
rect->w = (float) render_size.height * content_size.width
/ content_size.height;
rect->x = (render_size.width - rect->w) / 2.f;
}
}
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
// Only upscale video frames, not icon
bool can_upscale = screen->video && !screen->disconnected;
struct sc_size render_size =
sc_sdl_get_render_output_size(screen->renderer);
compute_content_rect(render_size, screen->content_size, can_upscale,
screen->render_fit, &screen->rect);
}
// render the texture to the renderer
//
// Set the update_content_rect flag if the window or content size may have
// changed, so that the content rectangle is recomputed
static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
assert(screen->window_shown);
if (update_content_rect) {
sc_screen_update_content_rect(screen);
}
SDL_Renderer *renderer = screen->renderer;
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
sc_sdl_render_clear(renderer);
bool ok = false;
SDL_Texture *texture = screen->tex.texture;
if (!texture) {
// Draw a dark 10x10 square in the top-right corner to distinguish a
// black frame from the absence of a frame
struct sc_size render_size = sc_sdl_get_render_output_size(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0x33, 0xff);
SDL_FRect rect = {
.x = render_size.width - 20,
.y = 10,
.w = 10,
.h = 10,
};
SDL_RenderFillRect(renderer, &rect);
goto end;
}
SDL_FRect *geometry = &screen->rect;
enum sc_orientation orientation = screen->orientation;
if (orientation == SC_ORIENTATION_0) {
ok = SDL_RenderTexture(renderer, texture, NULL, geometry);
} else {
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
double angle = 90 * cw_rotation;
const SDL_FRect *dstrect = NULL;
SDL_FRect rect;
if (sc_orientation_is_swap(orientation)) {
rect.x = geometry->x + (geometry->w - geometry->h) / 2.f;
rect.y = geometry->y + (geometry->h - geometry->w) / 2.f;
rect.w = geometry->h;
rect.h = geometry->w;
dstrect = &rect;
} else {
dstrect = geometry;
}
SDL_FlipMode flip = sc_orientation_is_mirror(orientation)
? SDL_FLIP_HORIZONTAL : 0;
ok = SDL_RenderTextureRotated(renderer, texture, NULL, dstrect, angle,
NULL, flip);
}
if (!ok) {
LOGE("Could not render texture: %s", SDL_GetError());
}
end:
sc_sdl_render_present(renderer);
}
static void
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);
}
}
}
#if defined(__APPLE__) || defined(_WIN32)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
#ifdef CONTINUOUS_RESIZING_WORKAROUND
// On Windows and MacOS, resizing blocks the event loop, so resizing events are
// not triggered. As a workaround, handle them in an event handler.
//
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
// <https://stackoverflow.com/a/40693139/1987178>
static bool
event_watcher(void *data, SDL_Event *event) {
struct sc_screen *screen = data;
assert(screen->video);
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, &event->window);
}
return true;
}
#endif
static bool
sc_screen_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx,
const struct sc_stream_session *session) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
struct sc_screen *screen = DOWNCAST(sink);
if (ctx->width <= 0 || ctx->width > 0xFFFF
|| ctx->height <= 0 || ctx->height > 0xFFFF) {
LOGE("Invalid video size: %dx%d", ctx->width, ctx->height);
return false;
}
// content_size can be written from this thread, because it is never read
// from the main thread before handling SC_EVENT_OPEN_WINDOW (which acts as
// a synchronization point) when video is enabled
screen->frame_size.width = session->video.width;
screen->frame_size.height = session->video.height;
screen->content_size = get_oriented_size(screen->frame_size,
screen->orientation);
screen->current_session = *session;
bool ok = sc_push_event(SC_EVENT_OPEN_WINDOW);
if (!ok) {
return false;
}
#ifndef NDEBUG
screen->open = true;
#endif
// nothing to do, the screen is already open on the main thread
return true;
}
static void
sc_screen_frame_sink_close(struct sc_frame_sink *sink) {
struct sc_screen *screen = DOWNCAST(sink);
(void) screen;
#ifndef NDEBUG
screen->open = false;
#endif
// nothing to do, the screen lifecycle is not managed by the frame producer
}
static bool
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
struct sc_screen *screen = DOWNCAST(sink);
assert(screen->video);
sc_mutex_lock(&screen->mutex);
bool previous_skipped = sc_frame_buffer_has_frame(&screen->fb);
bool ok = sc_frame_buffer_push(&screen->fb, frame);
screen->prevent_auto_resize = screen->current_session.video.client_resized;
sc_mutex_unlock(&screen->mutex);
if (!ok) {
return false;
}
if (previous_skipped) {
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
// this new frame instead
} else {
// Post the event on the UI thread
bool ok = sc_push_event(SC_EVENT_NEW_FRAME);
if (!ok) {
return false;
}
}
return true;
}
static bool
sc_screen_frame_sink_push_session(struct sc_frame_sink *sink,
const struct sc_stream_session *session) {
struct sc_screen *screen = DOWNCAST(sink);
screen->current_session = *session;
return true;
}
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;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
screen->disconnected = false;
screen->disconnect_started = false;
screen->video = params->video;
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;
screen->req.width = params->window_width;
screen->req.height = params->window_height;
screen->req.fullscreen = params->fullscreen;
screen->req.start_fps_counter = params->start_fps_counter;
screen->prevent_auto_resize = false;
bool ok = sc_mutex_init(&screen->mutex);
if (!ok) {
return false;
}
ok = sc_frame_buffer_init(&screen->fb);
if (!ok) {
goto error_destroy_mutex;
}
if (!sc_fps_counter_init(&screen->fps_counter)) {
goto error_destroy_frame_buffer;
}
if (screen->video) {
screen->orientation = params->orientation;
if (screen->orientation != SC_ORIENTATION_0) {
LOGI("Initial display orientation set to %s",
sc_orientation_get_name(screen->orientation));
}
}
// Always create the window hidden to prevent blinking during initialization
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_HIDDEN;
if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
}
if (params->window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS;
}
if (params->video) {
// The window will be shown on first frame
window_flags |= SDL_WINDOW_RESIZABLE;
}
const char *title = params->window_title;
assert(title);
int x = SDL_WINDOWPOS_UNDEFINED;
int y = SDL_WINDOWPOS_UNDEFINED;
int width = 256;
int height = 256;
if (params->window_x != SC_WINDOW_POSITION_UNDEFINED) {
x = params->window_x;
}
if (params->window_y != SC_WINDOW_POSITION_UNDEFINED) {
y = params->window_y;
}
if (params->window_width) {
width = params->window_width;
}
if (params->window_height) {
height = params->window_height;
}
// The window will be positioned and sized on first video frame
screen->window =
sc_sdl_create_window(title, x, y, width, height, window_flags);
if (!screen->window) {
LOGE("Could not create window: %s", SDL_GetError());
goto error_destroy_fps_counter;
}
screen->renderer = SDL_CreateRenderer(screen->window, NULL);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window;
}
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
screen->gl_context = NULL;
// starts with "opengl"
const char *renderer_name = SDL_GetRendererName(screen->renderer);
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
// Persuade macOS to give us something better than OpenGL 2.1.
// If we create a Core Profile context, we get the best OpenGL version.
bool ok = SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
if (!ok) {
LOGW("Could not set a GL Core Profile Context");
}
LOGD("Creating OpenGL Core Profile context");
screen->gl_context = SDL_GL_CreateContext(screen->window);
if (!screen->gl_context) {
LOGE("Could not create OpenGL context: %s", SDL_GetError());
goto error_destroy_renderer;
}
}
#endif
bool mipmaps = params->video;
ok = sc_texture_init(&screen->tex, screen->renderer, mipmaps);
if (!ok) {
goto error_destroy_renderer;
}
ok = SDL_StartTextInput(screen->window);
if (!ok) {
LOGE("Could not enable text input: %s", SDL_GetError());
goto error_destroy_texture;
}
SDL_Surface *icon = sc_icon_load(SC_ICON_FILENAME_SCRCPY);
if (icon) {
if (!SDL_SetWindowIcon(screen->window, icon)) {
LOGW("Could not set window icon: %s", SDL_GetError());
}
if (!params->video) {
screen->content_size.width = icon->w;
screen->content_size.height = icon->h;
ok = sc_texture_set_from_surface(&screen->tex, icon);
if (!ok) {
LOGE("Could not set icon: %s", SDL_GetError());
}
}
sc_icon_destroy(icon);
} else {
// not fatal
LOGE("Could not load icon");
if (!params->video) {
// Make sure the content size is initialized
screen->content_size.width = 256;
screen->content_size.height = 256;
}
}
screen->frame = av_frame_alloc();
if (!screen->frame) {
LOG_OOM();
goto error_destroy_texture;
}
struct sc_input_manager_params im_params = {
.controller = params->controller,
.fp = params->fp,
.screen = screen,
.kp = params->kp,
.mp = params->mp,
.gp = params->gp,
.camera = params->camera,
.mouse_bindings = params->mouse_bindings,
.legacy_paste = params->legacy_paste,
.clipboard_autosync = params->clipboard_autosync,
.shortcut_mods = params->shortcut_mods,
};
sc_input_manager_init(&screen->im, &im_params);
// Initialize even if not used for simplicity
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
#ifdef CONTINUOUS_RESIZING_WORKAROUND
if (screen->video) {
ok = SDL_AddEventWatch(event_watcher, screen);
if (!ok) {
LOGW("Could not add event watcher for continuous resizing: %s",
SDL_GetError());
}
}
#endif
memset(&screen->current_session, 0, sizeof(screen->current_session));
static const struct sc_frame_sink_ops ops = {
.open = sc_screen_frame_sink_open,
.close = sc_screen_frame_sink_close,
.push = sc_screen_frame_sink_push,
.push_session = sc_screen_frame_sink_push_session,
};
screen->frame_sink.ops = &ops;
#ifndef NDEBUG
screen->open = false;
#endif
if (!screen->video) {
// Show the window immediately
screen->window_shown = true;
sc_sdl_show_window(screen->window);
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse immediately if video mirroring is disabled
sc_mouse_capture_set_active(&screen->mc, true);
}
}
return true;
error_destroy_texture:
sc_texture_destroy(&screen->tex);
error_destroy_renderer:
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
if (screen->gl_context) {
SDL_GL_DestroyContext(screen->gl_context);
}
#endif
SDL_DestroyRenderer(screen->renderer);
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_fps_counter:
sc_fps_counter_destroy(&screen->fps_counter);
error_destroy_frame_buffer:
sc_frame_buffer_destroy(&screen->fb);
error_destroy_mutex:
sc_mutex_destroy(&screen->mutex);
return false;
}
static void
sc_screen_show_initial_window(struct sc_screen *screen) {
int x = screen->req.x != SC_WINDOW_POSITION_UNDEFINED
? screen->req.x : (int) SDL_WINDOWPOS_CENTERED;
int y = screen->req.y != SC_WINDOW_POSITION_UNDEFINED
? screen->req.y : (int) SDL_WINDOWPOS_CENTERED;
struct sc_point position = {
.x = x,
.y = y,
};
struct sc_size window_size =
get_initial_optimal_size(screen->content_size, screen->req.width,
screen->req.height);
assert(is_windowed(screen));
set_aspect_ratio(screen, screen->content_size);
sc_sdl_set_window_size(screen->window, window_size);
sc_sdl_set_window_position(screen->window, position);
if (screen->req.fullscreen) {
sc_screen_toggle_fullscreen(screen);
}
if (screen->req.start_fps_counter) {
sc_fps_counter_start(&screen->fps_counter);
}
screen->window_shown = true;
sc_sdl_show_window(screen->window);
sc_screen_update_content_rect(screen);
}
void
sc_screen_hide_window(struct sc_screen *screen) {
sc_sdl_hide_window(screen->window);
screen->window_shown = false;
}
void
sc_screen_interrupt(struct sc_screen *screen) {
sc_fps_counter_interrupt(&screen->fps_counter);
}
static void
sc_screen_interrupt_disconnect(struct sc_screen *screen) {
if (screen->disconnect_started) {
sc_disconnect_interrupt(&screen->disconnect);
}
}
void
sc_screen_join(struct sc_screen *screen) {
sc_fps_counter_join(&screen->fps_counter);
if (screen->disconnect_started) {
sc_disconnect_join(&screen->disconnect);
}
}
void
sc_screen_destroy(struct sc_screen *screen) {
#ifndef NDEBUG
assert(!screen->open);
#endif
if (screen->disconnect_started) {
sc_disconnect_destroy(&screen->disconnect);
}
sc_texture_destroy(&screen->tex);
av_frame_free(&screen->frame);
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DestroyContext(screen->gl_context);
#endif
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
sc_fps_counter_destroy(&screen->fps_counter);
sc_frame_buffer_destroy(&screen->fb);
sc_mutex_destroy(&screen->mutex);
SDL_Event event;
int nevents = SDL_PeepEvents(&event, 1, SDL_GETEVENT,
SC_EVENT_DISCONNECTED_ICON_LOADED,
SC_EVENT_DISCONNECTED_ICON_LOADED);
if (nevents == 1) {
assert(event.type == SC_EVENT_DISCONNECTED_ICON_LOADED);
// The event was posted, but not handled, the icon must be freed
SDL_Surface *dangling_icon = event.user.data1;
sc_icon_destroy(dangling_icon);
}
}
static void
resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
struct sc_size new_content_size) {
assert(screen->video);
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
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));
set_aspect_ratio(screen, new_content_size);
sc_sdl_set_window_size(screen->window, target_size);
}
static void
set_content_size(struct sc_screen *screen, struct sc_size new_content_size,
bool resize) {
assert(screen->video);
if (resize) {
if (is_windowed(screen)) {
resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) {
// Store the windowed size to be able to compute the optimal size
// once fullscreen/maximized/minimized are disabled
screen->windowed_content_size = screen->content_size;
screen->resize_pending = true;
}
}
screen->content_size = new_content_size;
}
static void
apply_pending_resize(struct sc_screen *screen) {
assert(screen->video);
assert(is_windowed(screen));
if (screen->resize_pending) {
resize_for_content(screen, screen->windowed_content_size,
screen->content_size);
screen->resize_pending = false;
}
}
void
sc_screen_set_orientation(struct sc_screen *screen,
enum sc_orientation orientation) {
assert(screen->video);
if (orientation == screen->orientation) {
return;
}
struct sc_size new_content_size =
get_oriented_size(screen->frame_size, orientation);
set_content_size(screen, new_content_size, true);
screen->orientation = orientation;
LOGI("Display orientation set to %s", sc_orientation_get_name(orientation));
sc_screen_render(screen, true);
}
static bool
sc_screen_apply_frame(struct sc_screen *screen, bool can_resize) {
assert(screen->video);
assert(screen->window_shown);
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
AVFrame *frame = screen->frame;
struct sc_size new_frame_size = {frame->width, frame->height};
if (screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
// frame dimension changed
screen->frame_size = new_frame_size;
struct sc_size new_content_size =
get_oriented_size(new_frame_size, screen->orientation);
set_content_size(screen, new_content_size, can_resize);
sc_screen_update_content_rect(screen);
}
bool ok = sc_texture_set_from_frame(&screen->tex, frame);
if (!ok) {
return false;
}
sc_screen_render(screen, false);
return true;
}
static bool
sc_screen_update_frame(struct sc_screen *screen) {
assert(screen->video);
if (screen->paused) {
if (!screen->resume_frame) {
screen->resume_frame = av_frame_alloc();
if (!screen->resume_frame) {
LOG_OOM();
return false;
}
} else {
av_frame_unref(screen->resume_frame);
}
sc_mutex_lock(&screen->mutex);
sc_frame_buffer_consume(&screen->fb, screen->resume_frame);
sc_mutex_unlock(&screen->mutex);
return true;
}
av_frame_unref(screen->frame);
sc_mutex_lock(&screen->mutex);
sc_frame_buffer_consume(&screen->fb, screen->frame);
// read with lock held
bool can_resize = !screen->prevent_auto_resize;
sc_mutex_unlock(&screen->mutex);
return sc_screen_apply_frame(screen, can_resize);
}
void
sc_screen_set_paused(struct sc_screen *screen, bool paused) {
assert(screen->video);
if (!paused && !screen->paused) {
// nothing to do
return;
}
if (screen->paused && screen->resume_frame) {
// If display screen was paused, refresh the frame immediately, even if
// the new state is also paused.
av_frame_free(&screen->frame);
screen->frame = screen->resume_frame;
screen->resume_frame = NULL;
bool ok = sc_screen_apply_frame(screen, true);
if (!ok) {
LOGE("Resume frame update failed");
}
}
if (!paused) {
LOGI("Display screen unpaused");
} else if (!screen->paused) {
LOGI("Display screen paused");
} else {
LOGI("Display screen re-paused");
}
screen->paused = paused;
}
void
sc_screen_toggle_fullscreen(struct sc_screen *screen) {
assert(screen->video);
bool req_fullscreen =
!(SDL_GetWindowFlags(screen->window) & SDL_WINDOW_FULLSCREEN);
bool ok = SDL_SetWindowFullscreen(screen->window, req_fullscreen);
if (!ok) {
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
return;
}
LOGD("Requested %s mode", req_fullscreen ? "fullscreen" : "windowed");
}
void
sc_screen_resize_to_fit(struct sc_screen *screen) {
assert(screen->video);
if (!is_windowed(screen)) {
return;
}
struct sc_point point = sc_sdl_get_window_position(screen->window);
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
struct sc_size optimal_size =
get_optimal_size(window_size, screen->content_size, false);
// Center the window related to the device screen
assert(optimal_size.width <= window_size.width);
assert(optimal_size.height <= window_size.height);
struct sc_point new_position = {
.x = point.x + (window_size.width - optimal_size.width) / 2,
.y = point.y + (window_size.height - optimal_size.height) / 2,
};
set_aspect_ratio(screen, screen->content_size);
sc_sdl_set_window_size(screen->window, optimal_size);
sc_sdl_set_window_position(screen->window, new_position);
LOGD("Resized to optimal size: %ux%u", optimal_size.width,
optimal_size.height);
}
void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
assert(screen->video);
if (!is_windowed(screen)) {
return;
}
struct sc_size content_size = screen->content_size;
set_aspect_ratio(screen, content_size);
sc_sdl_set_window_size(screen->window, content_size);
LOGD("Resized to pixel-perfect: %ux%u", content_size.width,
content_size.height);
}
static void
sc_disconnect_on_icon_loaded(struct sc_disconnect *d, SDL_Surface *icon,
void *userdata) {
(void) d;
(void) userdata;
bool ok = sc_push_event_with_data(SC_EVENT_DISCONNECTED_ICON_LOADED, icon);
if (!ok) {
sc_icon_destroy(icon);
}
}
static void
sc_disconnect_on_timeout(struct sc_disconnect *d, void *userdata) {
(void) d;
(void) userdata;
bool ok = sc_push_event(SC_EVENT_DISCONNECTED_TIMEOUT);
(void) ok; // ignore failure
}
void
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
switch (event->type) {
case SC_EVENT_OPEN_WINDOW:
sc_screen_show_initial_window(screen);
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse on start
sc_mouse_capture_set_active(&screen->mc, true);
}
sc_screen_render(screen, false);
return;
case SC_EVENT_NEW_FRAME: {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
LOGE("Frame update failed\n");
}
return;
}
case SDL_EVENT_WINDOW_EXPOSED:
sc_screen_render(screen, true);
return;
// 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, &event->window);
return;
#endif
case SDL_EVENT_WINDOW_RESTORED:
if (screen->video && is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return;
case SDL_EVENT_WINDOW_ENTER_FULLSCREEN:
LOGD("Switched to fullscreen mode");
assert(screen->video);
return;
case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN:
LOGD("Switched to windowed mode");
assert(screen->video);
if (is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return;
case SC_EVENT_DEVICE_DISCONNECTED:
assert(!screen->disconnected);
screen->disconnected = true;
if (!screen->window_shown) {
// No window open
return;
}
sc_input_manager_handle_event(&screen->im, event);
sc_texture_reset(&screen->tex);
sc_screen_render(screen, true);
sc_tick deadline = sc_tick_now() + SC_TICK_FROM_SEC(2);
static const struct sc_disconnect_callbacks cbs = {
.on_icon_loaded = sc_disconnect_on_icon_loaded,
.on_timeout = sc_disconnect_on_timeout,
};
bool ok =
sc_disconnect_start(&screen->disconnect, deadline, &cbs, NULL);
if (ok) {
screen->disconnect_started = true;
}
return;
}
if (sc_screen_is_relative_mode(screen)
&& sc_mouse_capture_handle_event(&screen->mc, event)) {
// The mouse capture handler consumed the event
return;
}
sc_input_manager_handle_event(&screen->im, event);
}
void
sc_screen_handle_disconnection(struct sc_screen *screen) {
if (!screen->window_shown) {
// No window open, quit immediately
return;
}
if (!screen->disconnect_started) {
// If sc_disconnect_start() failed, quit immediately
return;
}
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case SDL_EVENT_WINDOW_EXPOSED:
sc_screen_render(screen, true);
break;
case SC_EVENT_DISCONNECTED_ICON_LOADED: {
SDL_Surface *icon_disconnected = event.user.data1;
assert(icon_disconnected);
bool ok = sc_texture_set_from_surface(&screen->tex,
icon_disconnected);
if (ok) {
screen->content_size.width = icon_disconnected->w;
screen->content_size.height = icon_disconnected->h;
sc_screen_render(screen, true);
} else {
// not fatal
LOGE("Could not set disconnected icon");
}
sc_icon_destroy(icon_disconnected);
break;
}
case SC_EVENT_DISCONNECTED_TIMEOUT:
LOGD("Closing after device disconnection");
return;
case SDL_EVENT_QUIT:
LOGD("User requested to quit");
sc_screen_interrupt_disconnect(screen);
return;
default:
sc_input_manager_handle_event(&screen->im, &event);
}
}
}
struct sc_point
sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen,
int32_t x, int32_t y) {
assert(screen->video);
enum sc_orientation orientation = screen->orientation;
int32_t w = screen->content_size.width;
int32_t h = screen->content_size.height;
// 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;
struct sc_point result;
switch (orientation) {
case SC_ORIENTATION_0:
result.x = x;
result.y = y;
break;
case SC_ORIENTATION_90:
result.x = y;
result.y = w - x;
break;
case SC_ORIENTATION_180:
result.x = w - x;
result.y = h - y;
break;
case SC_ORIENTATION_270:
result.x = h - y;
result.y = x;
break;
case SC_ORIENTATION_FLIP_0:
result.x = w - x;
result.y = y;
break;
case SC_ORIENTATION_FLIP_90:
result.x = h - y;
result.y = w - x;
break;
case SC_ORIENTATION_FLIP_180:
result.x = x;
result.y = h - y;
break;
default:
assert(orientation == SC_ORIENTATION_FLIP_270);
result.x = y;
result.y = x;
break;
}
return result;
}
struct sc_point
sc_screen_convert_window_to_frame_coords(struct sc_screen *screen,
int32_t x, int32_t y) {
sc_screen_hidpi_scale_coords(screen, &x, &y);
return sc_screen_convert_drawable_to_frame_coords(screen, x, y);
}
void
sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y) {
// take the HiDPI scaling (dw/ww and dh/wh) into account
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
int64_t ww = window_size.width;
int64_t wh = window_size.height;
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(screen->window);
int64_t dw = drawable_size.width;
int64_t dh = drawable_size.height;
// scale for HiDPI (64 bits for intermediate multiplications)
*x = (int64_t) *x * dw / ww;
*y = (int64_t) *y * dh / wh;
}