/* * This software is licensed under the terms of the MIT License. * See COPYING for further information. * --- * Copyright (c) 2011-2024, Lukas Weber . * Copyright (c) 2012-2024, Andrei Alexeyev . */ #include "video.h" #include "dynarray.h" #include "events.h" #include "global.h" #include "renderer/api.h" #include "rwops/rwops_autobuf.h" #include "stagedraw.h" #include "taskmanager.h" #include "util/env.h" #include "util/fbmgr.h" #include "util/graphics.h" #include "util/io.h" #include "version.h" #include "video_postprocess.h" typedef DYNAMIC_ARRAY(VideoMode) VideoModeArray; typedef enum FramedumpSource { FRAMEDUMP_SRC_SCREEN, FRAMEDUMP_SRC_VIEWPORT, } FramedumpSource; static struct { VideoModeArray fs_modes; VideoModeArray win_modes; SDL_Window *window; VideoPostProcess *postprocess; VideoMode intended; VideoMode current; VideoBackend backend; double scaling_factor; uint num_resize_events; struct { char *name_prefix; size_t name_prefix_len; size_t frame_count; int compression; FramedumpSource source; } framedump; } video; #define FRAMEDUMP_FILENAME_SUFFIX ".png" #define FRAMEDUMP_FILENAME_NUM_DIGITS 8 #define FRAMEDUMP_FILENAME_EXTRA_BUFSIZE \ (FRAMEDUMP_FILENAME_NUM_DIGITS + sizeof(FRAMEDUMP_FILENAME_SUFFIX)) #define FRAMEDUMP_FILENAME_FORMAT "%08u" FRAMEDUMP_FILENAME_SUFFIX VideoCapabilityState (*video_query_capability)(VideoCapability cap); typedef struct ScreenshotTaskData { char *dest_path; // NULL if in framedump mode Pixmap image; uint32_t frame_num; // for framedump mode only } ScreenshotTaskData; #define VIDEO_MIN_SIZE_FACTOR 0.8 #define VIDEO_MIN_WIDTH (int)(SCREEN_W * VIDEO_MIN_SIZE_FACTOR) #define VIDEO_MIN_HEIGHT (int)(SCREEN_H * VIDEO_MIN_SIZE_FACTOR) /* * BEGIN Conversion between screen-space and pixel-space coordinates (for high-DPI mode) * TODO: figure out how to round these correctly */ attr_unused static inline int coords_val_screen_to_pixels(int screen_coord) { return round(screen_coord * video.scaling_factor); } attr_unused static inline int coords_val_pixels_to_screen(int pixel_coord) { return round(pixel_coord / video.scaling_factor); } attr_unused static inline IntOffset coords_ofs_screen_to_pixels(IntOffset screen_ofs) { IntOffset pixel_ofs; pixel_ofs.x = coords_val_screen_to_pixels(screen_ofs.x); pixel_ofs.y = coords_val_screen_to_pixels(screen_ofs.y); return pixel_ofs; } attr_unused static inline IntOffset coords_ofs_pixels_to_screen(IntOffset pixel_ofs) { IntOffset screen_ofs; screen_ofs.x = coords_val_pixels_to_screen(pixel_ofs.x); screen_ofs.y = coords_val_pixels_to_screen(pixel_ofs.y); return screen_ofs; } attr_unused static inline IntExtent coords_ext_screen_to_pixels(IntExtent screen_ext) { IntExtent pixel_ext; pixel_ext.w = coords_val_screen_to_pixels(screen_ext.w); pixel_ext.h = coords_val_screen_to_pixels(screen_ext.h); return pixel_ext; } attr_unused static inline IntExtent coords_ext_pixels_to_screen(IntExtent pixel_ext) { IntExtent screen_ext; screen_ext.w = coords_val_pixels_to_screen(pixel_ext.w); screen_ext.h = coords_val_pixels_to_screen(pixel_ext.h); return screen_ext; } attr_unused static inline IntRect coords_rect_screen_to_pixels(IntRect screen_rect) { IntRect pixel_rect; pixel_rect.extent = coords_ext_screen_to_pixels(screen_rect.extent); pixel_rect.offset = coords_ofs_screen_to_pixels(screen_rect.offset); return pixel_rect; } attr_unused static inline IntRect coords_rect_pixels_to_screen(IntRect pixel_rect) { IntRect screen_rect; screen_rect.extent = coords_ext_pixels_to_screen(pixel_rect.extent); screen_rect.offset = coords_ofs_pixels_to_screen(pixel_rect.offset); return screen_rect; } /* * END Conversion between screen-space and pixel-space coordinates (for high-DPI mode) */ static VideoCapabilityState video_query_capability_generic(VideoCapability cap) { switch(cap) { case VIDEO_CAP_FULLSCREEN: return VIDEO_AVAILABLE; case VIDEO_CAP_EXTERNAL_RESIZE: return video_is_fullscreen() ? VIDEO_CURRENTLY_UNAVAILABLE : VIDEO_AVAILABLE; case VIDEO_CAP_CHANGE_RESOLUTION: return video_is_fullscreen() ? VIDEO_CURRENTLY_UNAVAILABLE : VIDEO_AVAILABLE; case VIDEO_CAP_VSYNC_ADAPTIVE: return VIDEO_AVAILABLE; } UNREACHABLE; } static VideoCapabilityState video_query_capability_alwaysfullscreen(VideoCapability cap) { switch(cap) { case VIDEO_CAP_FULLSCREEN: return VIDEO_ALWAYS_ENABLED; case VIDEO_CAP_EXTERNAL_RESIZE: return VIDEO_NEVER_AVAILABLE; // XXX: Might not be actually working, but let's be optimistic. case VIDEO_CAP_CHANGE_RESOLUTION: return VIDEO_AVAILABLE; case VIDEO_CAP_VSYNC_ADAPTIVE: return VIDEO_AVAILABLE; } UNREACHABLE; } static VideoCapabilityState video_query_capability_switch(VideoCapability cap) { switch(cap) { // We want the window to be resizable and resized internally by SDL // when the Switch gets docked/undocked case VIDEO_CAP_FULLSCREEN: return VIDEO_NEVER_AVAILABLE; case VIDEO_CAP_EXTERNAL_RESIZE: return VIDEO_AVAILABLE; case VIDEO_CAP_CHANGE_RESOLUTION: return VIDEO_NEVER_AVAILABLE; case VIDEO_CAP_VSYNC_ADAPTIVE: return VIDEO_NEVER_AVAILABLE; } UNREACHABLE; } static VideoCapabilityState video_query_capability_webcanvas(VideoCapability cap) { switch(cap) { case VIDEO_CAP_EXTERNAL_RESIZE: return VIDEO_ALWAYS_ENABLED; case VIDEO_CAP_FULLSCREEN: return VIDEO_NEVER_AVAILABLE; case VIDEO_CAP_CHANGE_RESOLUTION: return VIDEO_NEVER_AVAILABLE; case VIDEO_CAP_VSYNC_ADAPTIVE: return VIDEO_NEVER_AVAILABLE; default: return video_query_capability_generic(cap); } } static VideoCapabilityState (*video_query_capability_kiosk_fallback)(VideoCapability cap); static VideoCapabilityState video_query_capability_kiosk(VideoCapability cap) { switch(cap) { case VIDEO_CAP_FULLSCREEN: return VIDEO_ALWAYS_ENABLED; case VIDEO_CAP_CHANGE_RESOLUTION: return VIDEO_NEVER_AVAILABLE; case VIDEO_CAP_EXTERNAL_RESIZE: return VIDEO_NEVER_AVAILABLE; default: return video_query_capability_kiosk_fallback(cap); } } static void video_add_mode(VideoModeArray *mode_array, IntExtent mode_screen, IntExtent min_screen, IntExtent max_screen, const char *mode_type) { if( (mode_screen.w > max_screen.w && max_screen.w > 0) || (mode_screen.h > max_screen.h && max_screen.h > 0) ) { log_debug("Mode %ix%i rejected: > %ix%i", mode_screen.w, mode_screen.h, max_screen.w, max_screen.h); return; } if( mode_screen.w < min_screen.w || mode_screen.h < min_screen.h ) { log_debug("Mode %ix%i rejected: < %ix%i", mode_screen.w, mode_screen.h, min_screen.w, min_screen.h); return; } for(uint i = 0; i < mode_array->num_elements; ++i) { VideoMode *m = mode_array->data + i; if(m->width == mode_screen.w && m->height == mode_screen.h) { log_debug("Mode %ix%i rejected: already registered", mode_screen.w, mode_screen.h); return; } } dynarray_append(mode_array, { .as_int_extent = mode_screen }); log_debug("Add %s mode: %ix%i", mode_type, mode_screen.w, mode_screen.h); } static void video_add_mode_dpi_aware(VideoModeArray *mode_array, IntExtent mode_pix, IntExtent min_screen, IntExtent max_screen, const char *mode_type) { IntExtent mode_screen = coords_ext_pixels_to_screen(mode_pix); // Yes, we add both. The pixel-space size is interpreted as screen-space. // Anything too large to fit on the screen is rejected. video_add_mode(mode_array, mode_screen, min_screen, max_screen, mode_type); video_add_mode(mode_array, mode_pix, min_screen, max_screen, mode_type); } static void video_add_mode_fullscreen(IntExtent mode_pix, IntExtent min_screen, IntExtent max_screen) { video_add_mode_dpi_aware(&video.fs_modes, mode_pix, min_screen, max_screen, "fullscreen"); } static void video_add_mode_windowed(IntExtent mode_pix, IntExtent min_screen, IntExtent max_screen) { video_add_mode_dpi_aware(&video.win_modes, mode_pix, min_screen, max_screen, "windowed"); } static int video_compare_modes(const void *a, const void *b) { const VideoMode *va = a; const VideoMode *vb = b; return va->width * va->height - vb->width * vb->height; } static IntExtent video_get_screen_framebuffer_size(void) { return r_framebuffer_get_size(NULL); } static FloatExtent video_get_viewport_size_for_framebuffer(IntExtent framebuffer_size) { float w = framebuffer_size.w; float h = framebuffer_size.h; float r = w / h; if(r > VIDEO_ASPECT_RATIO) { w = h * VIDEO_ASPECT_RATIO; } else if(r < VIDEO_ASPECT_RATIO) { h = w / VIDEO_ASPECT_RATIO; } return (FloatExtent) { w, h }; } static IntExtent round_viewport_size(FloatExtent vp) { return (IntExtent) { round(vp.w), round(vp.h) }; } static void video_update_mode_lists(void) { video.fs_modes.num_elements = 0; video.win_modes.num_elements = 0; dynarray_ensure_capacity(&video.fs_modes, 16); dynarray_ensure_capacity(&video.win_modes, 16); bool fullscreen_available = false; bool has_windowed_modes = (video_query_capability(VIDEO_CAP_FULLSCREEN) != VIDEO_ALWAYS_ENABLED); FloatExtent largest_fullscreen_viewport = { 0, 0 }; IntExtent screenspace_min_size = { VIDEO_MIN_WIDTH, VIDEO_MIN_HEIGHT }; screenspace_min_size = coords_ext_pixels_to_screen(screenspace_min_size); // Register all resolutions that are available in fullscreen and their corresponding windowed modes. for(int s = 0; s < video_num_displays(); ++s) { log_info("Found display #%i: %s", s, video_display_name(s)); SDL_DisplayMode desktop_mode; IntExtent screenspace_max_size = { 0 }; if(SDL_GetDesktopDisplayMode(s, &desktop_mode)) { log_sdl_error(LOG_WARN, "SDL_GetDesktopDisplayMode"); } else { log_debug("Desktop mode: %ix%i@%iHz", desktop_mode.w, desktop_mode.h, desktop_mode.refresh_rate); screenspace_max_size.w = desktop_mode.w; screenspace_max_size.h = desktop_mode.h; screenspace_max_size = coords_ext_pixels_to_screen(screenspace_max_size); log_debug("Scaled screen-space bounds: %ix%i", screenspace_max_size.w, screenspace_max_size.h); } for(int i = 0; i < SDL_GetNumDisplayModes(s); ++i) { SDL_DisplayMode mode = { SDL_PIXELFORMAT_UNKNOWN, 0, 0, 0, 0 }; if(SDL_GetDisplayMode(s, i, &mode) != 0) { log_sdl_error(LOG_WARN, "SDL_GetDisplayMode"); } else { log_debug("Display mode #%i: %ix%i@%iHz", i, mode.w, mode.h, mode.refresh_rate); video_add_mode_fullscreen((IntExtent) { mode.w, mode.h }, screenspace_min_size, screenspace_max_size); fullscreen_available = true; if(has_windowed_modes) { FloatExtent vp = video_get_viewport_size_for_framebuffer((IntExtent) { mode.w, mode.h }); video_add_mode_windowed(round_viewport_size(vp), screenspace_min_size, screenspace_max_size); // the ratio is always constant, so we need to check only 1 dimension if(vp.w > largest_fullscreen_viewport.w) { largest_fullscreen_viewport = vp; } } } } } if(has_windowed_modes) { // Insert some more windowed modes derived from our "ideal" resolution. // This is the resolution that the assets are optimized for. float ideal_factor = 2; FloatExtent ideal_resolution = { SCREEN_W * ideal_factor, SCREEN_H * ideal_factor }; if(largest_fullscreen_viewport.w == 0) { // no way to determine the upper bound; guess it largest_fullscreen_viewport = ideal_resolution; } IntExtent screenspace_max_size = coords_ext_pixels_to_screen(round_viewport_size(largest_fullscreen_viewport)); float scaling_factor = 0.5; float scaling_factor_step = 0.25; while(ideal_resolution.w * scaling_factor <= largest_fullscreen_viewport.w) { FloatExtent vp = { ideal_resolution.w * scaling_factor, ideal_resolution.h * scaling_factor, }; IntExtent pix_vp = round_viewport_size(vp); video_add_mode_windowed(pix_vp, screenspace_min_size, screenspace_max_size); scaling_factor += scaling_factor_step; } // Finally add the worst size we will tolerate, in case we haven't already. video_add_mode_windowed((IntExtent) { VIDEO_MIN_WIDTH, VIDEO_MIN_HEIGHT }, screenspace_min_size, screenspace_max_size); } dynarray_compact(&video.fs_modes); dynarray_compact(&video.win_modes); dynarray_qsort(&video.fs_modes, video_compare_modes); dynarray_qsort(&video.win_modes, video_compare_modes); if(!fullscreen_available) { log_warn("No available fullscreen modes"); config_set_int(CONFIG_FULLSCREEN, false); } } static void video_update_scaling_factor(void) { // NOTE: must query the main framebuffer explicitly here; postprocess buffers may have outdated information. IntExtent main_fb = r_framebuffer_get_size(NULL); assert(main_fb.w > 0); double scaling_factor = (double)main_fb.w / video.current.width; if(scaling_factor != video.scaling_factor) { log_debug("Scaling factor updated: %f --> %f", video.scaling_factor, scaling_factor); video.scaling_factor = scaling_factor; video_update_mode_lists(); IntExtent min_size = coords_ext_pixels_to_screen((IntExtent) { VIDEO_MIN_WIDTH, VIDEO_MIN_HEIGHT }); SDL_SetWindowMinimumSize(video.window, min_size.w, min_size.h); } } void video_get_viewport_size(float *width, float *height) { IntExtent fb = video_get_screen_framebuffer_size(); FloatExtent vp = video_get_viewport_size_for_framebuffer(fb); *width = vp.w; *height = vp.h; } void video_get_viewport(FloatRect *vp) { IntExtent fb = video_get_screen_framebuffer_size(); // vp->extent aliases vp->w and vp->h; see util/geometry.h vp->extent = video_get_viewport_size_for_framebuffer(fb); vp->x = (int)((fb.w - vp->w) * 0.5); vp->y = (int)((fb.h - vp->h) * 0.5); // This function can also be changed to return a FloatRect instead log_debug("current w/h: %dx%d", video.current.width, video.current.height); log_debug("viewport x/y: %fx%f", vp->x, vp->y); log_debug("viewport w/h: %fx%f", vp->w, vp->h); } static void video_set_viewport(void) { FloatRect vp; video_get_viewport(&vp); r_framebuffer_viewport_rect(NULL, vp); } static void video_update_vsync(void) { if(global.frameskip || config_get_int(CONFIG_VSYNC) == 0) { r_vsync(VSYNC_NONE); } else { switch(config_get_int(CONFIG_VSYNC)) { case 1: r_vsync(VSYNC_NORMAL); break; default: r_vsync(VSYNC_ADAPTIVE); break; } } } static void video_update_mode_settings(void) { SDL_ShowCursor(!video_is_fullscreen()); video_update_vsync(); SDL_GetWindowSize(video.window, &video.current.width, &video.current.height); video_set_viewport(); video_update_scaling_factor(); events_emit(TE_VIDEO_MODE_CHANGED, 0, NULL, NULL); } static const char *modeflagsstr(uint32_t flags) { if(flags & SDL_WINDOW_FULLSCREEN_DESKTOP) { return "fullscreen"; } else if(flags & SDL_WINDOW_RESIZABLE) { return "windowed, resizable"; } else { return "windowed"; } } static void video_new_window_internal(uint display, uint w, uint h, uint32_t flags, bool fallback) { if(video.window) { r_unclaim_window(video.window); SDL_DestroyWindow(video.window); video.window = NULL; video.num_resize_events = 0; } char title[sizeof(WINDOW_TITLE) + strlen(TAISEI_VERSION) + 2]; snprintf(title, sizeof(title), "%s v%s", WINDOW_TITLE, TAISEI_VERSION); video.window = r_create_window( title, SDL_WINDOWPOS_CENTERED_DISPLAY(display), SDL_WINDOWPOS_CENTERED_DISPLAY(display), w, h, flags | SDL_WINDOW_HIDDEN ); if(video.window) { SDL_ShowWindow(video.window); if(video.scaling_factor != 0) { IntExtent min_size = coords_ext_pixels_to_screen((IntExtent) { VIDEO_MIN_WIDTH, VIDEO_MIN_HEIGHT }); SDL_SetWindowMinimumSize(video.window, min_size.w, min_size.h); } video_update_mode_settings(); return; } if(fallback) { log_fatal("Failed to create window with mode %ix%i (%s): %s", w, h, modeflagsstr(flags), SDL_GetError()); return; } video_new_window_internal(display, RESX, RESY, flags & ~SDL_WINDOW_FULLSCREEN_DESKTOP, true); } static bool restrict_to_capability(bool enabled, VideoCapability cap) { VideoCapabilityState capval = video_query_capability(cap); switch(capval) { case VIDEO_ALWAYS_ENABLED: return true; case VIDEO_NEVER_AVAILABLE: return false; default: return enabled; } } static void video_new_window(uint display, uint w, uint h, bool fs, bool resizable) { uint32_t flags = SDL_WINDOW_ALLOW_HIGHDPI; fs = restrict_to_capability(fs, VIDEO_CAP_FULLSCREEN); resizable = restrict_to_capability(resizable, VIDEO_CAP_EXTERNAL_RESIZE); if(fs) { flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; } else if(resizable) { flags |= SDL_WINDOW_RESIZABLE; } video_new_window_internal(display, w, h, flags, false); display = video_current_display(); log_info("Created a new window: %ix%i (%s), on display #%i %s", video.current.width, video.current.height, modeflagsstr(SDL_GetWindowFlags(video.window)), display, video_display_name(display) ); events_pause_keyrepeat(); SDL_RaiseWindow(video.window); } static void video_set_fullscreen_internal(bool fullscreen) { uint32_t flags = fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0; events_pause_keyrepeat(); if(SDL_SetWindowFullscreen(video.window, flags) < 0) { log_error("Failed to switch to %s mode: %s", modeflagsstr(flags), SDL_GetError()); } SDL_RaiseWindow(video.window); } INLINE bool should_recreate_on_size_change(void) { bool defaultval = ( /* Resize failures are impossible to detect under some WMs */ video.backend == VIDEO_BACKEND_X11 || /* Needed to work around various SDL bugs and/or HTML/DOM quirks */ // video.backend == VIDEO_BACKEND_EMSCRIPTEN || 0); return env_get("TAISEI_VIDEO_RECREATE_ON_RESIZE", defaultval); } INLINE bool should_recreate_on_fullscreen_change(void) { bool defaultval = ( /* FIXME Do we need this? */ video.backend == VIDEO_BACKEND_X11 || 0); return env_get("TAISEI_VIDEO_RECREATE_ON_FULLSCREEN", defaultval); } void video_set_mode(uint display, uint w, uint h, bool fs, bool resizable) { fs = restrict_to_capability(fs, VIDEO_CAP_FULLSCREEN); resizable = restrict_to_capability(resizable, VIDEO_CAP_EXTERNAL_RESIZE); video.intended.width = w; video.intended.height = h; if(display >= video_num_displays()) { log_warn("Display index %u is invalid, falling back to 0 (%s)", display, video_display_name(0)); display = 0; } if(!video.window) { video_new_window(display, w, h, fs, resizable); return; } if( !restrict_to_capability(true, VIDEO_CAP_CHANGE_RESOLUTION) && video.current.width > 0 && video.current.height > 0 ) { w = video.current.width; h = video.current.height; } bool display_changed = display != video_current_display(); bool size_changed = w != video.current.width || h != video.current.height; bool fullscreen_changed = video_is_fullscreen() != fs; if(display_changed) { video_new_window(display, w, h, fs, resizable); return; } if(fullscreen_changed && should_recreate_on_fullscreen_change()) { video_new_window(display, w, h, fs, resizable); return; } if(size_changed && !fs) { if(!fullscreen_changed && should_recreate_on_size_change()) { video_new_window(display, w, h, fs, resizable); return; } SDL_SetWindowSize(video.window, w, h); SDL_SetWindowPosition( video.window, SDL_WINDOWPOS_CENTERED_DISPLAY(display), SDL_WINDOWPOS_CENTERED_DISPLAY(display) ); } video_set_fullscreen_internal(fs); SDL_SetWindowResizable(video.window, resizable); if(size_changed) { video_update_mode_settings(); } } SDL_Window *video_get_window(void) { return video.window; } void video_set_fullscreen(bool fullscreen) { if(video_query_capability(VIDEO_CAP_FULLSCREEN) != VIDEO_AVAILABLE) { return; } video_set_mode( SDL_GetWindowDisplayIndex(video.window), video.intended.width, video.intended.height, fullscreen, config_get_int(CONFIG_VID_RESIZABLE) ); } void video_set_display(uint idx) { video_set_mode( idx, video.intended.width, video.intended.height, config_get_int(CONFIG_FULLSCREEN), config_get_int(CONFIG_VID_RESIZABLE) ); } static void *video_screenshot_task(void *arg) { ScreenshotTaskData *tdata = arg; pixmap_convert_inplace_realloc(&tdata->image, PIXMAP_FORMAT_RGB8); PixmapPNGSaveOptions opts = PIXMAP_DEFAULT_PNG_SAVE_OPTIONS; if(tdata->dest_path) { opts.zlib_compression_level = 9; bool ok = pixmap_save_file(tdata->dest_path, &tdata->image, &opts.base); if(LIKELY(ok)) { char *syspath = vfs_repr(tdata->dest_path, true); log_info("Saved screenshot as %s", syspath); mem_free(syspath); } } else { // framedump mode char buf[video.framedump.name_prefix_len + FRAMEDUMP_FILENAME_EXTRA_BUFSIZE]; snprintf(buf, sizeof(buf), "%s" FRAMEDUMP_FILENAME_FORMAT, video.framedump.name_prefix, tdata->frame_num); SDL_RWops *stream = SDL_RWFromFile(buf, "wb"); if(UNLIKELY(!stream)) { log_sdl_error(LOG_ERROR, "SDL_RWFromFile"); return NULL; } opts.zlib_compression_level = video.framedump.compression; bool ok = pixmap_save_stream(stream, &tdata->image, &opts.base); SDL_RWclose(stream); if(LIKELY(ok)) { log_debug("Frame dump: %s", buf); } else { log_error("Frame dump failed: %s", buf); } } return NULL; } static void video_screenshot_free_task_data(void *arg) { ScreenshotTaskData *tdata = arg; mem_free(tdata->image.data.untyped); mem_free(tdata->dest_path); mem_free(tdata); } static void video_take_screenshot_callback(const Pixmap *px, void *userdata) { if(!px) { log_error("Failed to capture image"); return; } ScreenshotTaskData *tdata = userdata; // NOTE: could convert formats here to avoid a double copy, // but it's faster to do it on the task thread. pixmap_copy_alloc(px, &tdata->image); task_detach(taskmgr_global_submit((TaskParams) { .callback = video_screenshot_task, .userdata = userdata, .userdata_free_callback = video_screenshot_free_task_data, })); } void video_take_screenshot(bool viewport_only) { auto tdata = ALLOC(ScreenshotTaskData); SystemTime systime; char timestamp[FILENAME_TIMESTAMP_MIN_BUF_SIZE]; get_system_time(&systime); filename_timestamp(timestamp, sizeof(timestamp), systime); tdata->dest_path = strfmt("storage/screenshots/taisei_%s.png", timestamp); Framebuffer *fb = NULL; FramebufferAttachment attachment = FRAMEBUFFER_ATTACH_NONE; if(viewport_only && stage_draw_is_initialized()) { fb = stage_get_fbpair(FBPAIR_FG)->front; attachment = FRAMEBUFFER_ATTACH_COLOR0; } r_framebuffer_read_viewport_async( fb, attachment, tdata, video_take_screenshot_callback); } static void video_take_framedump(void) { Framebuffer *fb = NULL; FramebufferAttachment attachment = FRAMEBUFFER_ATTACH_NONE; if(video.framedump.source == FRAMEDUMP_SRC_VIEWPORT) { if(!stage_draw_is_initialized()) { return; } fb = stage_get_fbpair(FBPAIR_FG)->front; attachment = FRAMEBUFFER_ATTACH_COLOR0; } auto tdata = ALLOC(ScreenshotTaskData); tdata->frame_num = video.framedump.frame_count++; r_framebuffer_read_viewport_async( fb, attachment, tdata, video_take_screenshot_callback); } bool video_is_resizable(void) { return SDL_GetWindowFlags(video.window) & SDL_WINDOW_RESIZABLE; } bool video_is_fullscreen(void) { return SDL_GetWindowFlags(video.window) & SDL_WINDOW_FULLSCREEN_DESKTOP; } static void video_init_sdl(void) { // XXX: workaround for an SDL bug: https://bugzilla.libsdl.org/show_bug.cgi?id=4127 SDL_SetHintWithPriority(SDL_HINT_FRAMEBUFFER_ACCELERATION, "0", SDL_HINT_OVERRIDE); uint num_drivers = SDL_GetNumVideoDrivers(); const char *video_drivers[num_drivers]; void *buf; SDL_RWops *out = SDL_RWAutoBuffer(&buf, 256); SDL_RWprintf(out, "Available video drivers:"); for(uint i = 0; i < num_drivers; ++i) { video_drivers[i] = SDL_GetVideoDriver(i); SDL_RWprintf(out, " %s", video_drivers[i]); } SDL_WriteU8(out, 0); log_info("%s", (char*)buf); SDL_RWclose(out); // https://bugzilla.libsdl.org/show_bug.cgi?id=3948 // A suboptimal X11 server may be available on top of those systems, // so we push X11 down in the priority list. const char *prefer_drivers = env_get_string_nonempty("TAISEI_PREFER_SDL_VIDEODRIVERS", "wayland,cocoa,windows,x11"); const char *force_driver = env_get_string_nonempty("TAISEI_VIDEO_DRIVER", NULL); if(force_driver) { log_warn("TAISEI_VIDEO_DRIVER is deprecated and will be removed, use TAISEI_PREFER_SDL_VIDEODRIVERS or SDL_VIDEODRIVER instead"); } else { force_driver = env_get_string_nonempty("SDL_VIDEODRIVER", NULL); } if(prefer_drivers && *prefer_drivers && !force_driver) { char buf[strlen(prefer_drivers) + 1]; char *driver, *bufptr = buf; int drivernum = 0; strcpy(buf, prefer_drivers); while((driver = strtok_r(NULL, " :;,", &bufptr))) { bool skip = true; for(uint i = 0; i < num_drivers; ++i) { if(!strcmp(video_drivers[i], driver)) { skip = false; break; } } ++drivernum; if(skip) { continue; } log_info("Trying preferred driver #%i: %s", drivernum, driver); if(SDL_VideoInit(driver)) { log_info("Driver '%s' failed: %s", driver, SDL_GetError()); } else { return; } } if(drivernum) { log_info("All preferred drivers failed, falling back to SDL default"); } } if(SDL_VideoInit(force_driver)) { log_fatal("SDL_VideoInit() failed: %s", SDL_GetError()); } } static void video_handle_resize(int w, int h) { int minw, minh; SDL_GetWindowMinimumSize(video.window, &minw, &minh); if((w < minw || h < minh) && video.num_resize_events > 3) { log_warn("Bad resize: %ix%i is too small!", w, h); // FIXME: the video_new_window is actually a workaround for Wayland. // I'm not sure if it's necessary for anything else. video_new_window(video_current_display(), video.intended.width, video.intended.height, false, video_is_resizable()); return; } log_debug("%ix%i --> %ix%i", video.current.width, video.current.height, w, h); video.current.width = w; video.current.height = h; video_update_mode_settings(); ++video.num_resize_events; } static bool video_handle_window_event(SDL_Event *event, void *arg) { switch(event->window.event) { case SDL_WINDOWEVENT_SIZE_CHANGED: // This event is generated for any resizes, including calls to SDL_SetWindowSize. // It's followed by SDL_WINDOWEVENT_RESIZED for external resizes (from the WM or the // user). We only need to handle external resizes. log_debug("SDL_WINDOWEVENT_SIZE_CHANGED: %ix%i", event->window.data1, event->window.data2); // Catch resizes by the SDL portlib itself, when the console is docked/undocked // https://github.com/devkitPro/SDL/issues/31 if(video_get_backend() == VIDEO_BACKEND_SWITCH) { video_handle_resize(event->window.data1, event->window.data2); } break; case SDL_WINDOWEVENT_RESIZED: video_handle_resize(event->window.data1, event->window.data2); break; case SDL_WINDOWEVENT_FOCUS_LOST: if(config_get_int(CONFIG_FOCUS_LOSS_PAUSE)) { events_emit(TE_GAME_PAUSE, 0, NULL, NULL); } break; } return true; } static bool video_handle_config_event(SDL_Event *evt, void *arg) { ConfigValue *val = evt->user.data1; switch(evt->user.code) { case CONFIG_FULLSCREEN: video_set_fullscreen(val->i); break; case CONFIG_VID_DISPLAY: video_set_display(val->i); break; case CONFIG_VID_RESIZABLE: SDL_SetWindowResizable(video.window, val->i); break; case CONFIG_VSYNC: video_update_vsync(); break; } return false; } uint video_num_displays(void) { int displays = SDL_GetNumVideoDisplays(); if(displays < 1) { if(displays == 0) { log_warn("SDL_GetNumVideoDisplays() returned 0, this shouldn't happen"); } else { log_sdl_error(LOG_WARN, "SDL_GetNumVideoDisplays"); } displays = 1; } return displays; } const char *video_display_name(uint id) { assert(id < video_num_displays()); const char *name = SDL_GetDisplayName(id); if(name == NULL) { log_sdl_error(LOG_WARN, "SDL_GetDisplayName"); name = "Unknown"; } return name; } uint video_current_display(void) { int display = SDL_GetWindowDisplayIndex(video.window); if(display < 0) { log_sdl_error(LOG_WARN, "SDL_GetWindowDisplayIndex"); display = 1; } return display; } static void video_init_framedump(void) { const char *framedump_dir = env_get("TAISEI_FRAMEDUMP", NULL); const char *framedump_src = env_get("TAISEI_FRAMEDUMP_SOURCE", "screen"); if(framedump_dir == NULL) { return; } if(!strcasecmp(framedump_src, "screen")) { video.framedump.source = FRAMEDUMP_SRC_SCREEN; } else if(!strcasecmp(framedump_src, "viewport")) { video.framedump.source = FRAMEDUMP_SRC_VIEWPORT; } else { log_warn("Unknown source '%s'; assuming 'screen'", framedump_src); video.framedump.source = FRAMEDUMP_SRC_SCREEN; } video.framedump.compression = env_get("TAISEI_FRAMEDUMP_COMPRESSION", 1); video.framedump.name_prefix_len = strlen(framedump_dir); video.framedump.name_prefix = mem_alloc( video.framedump.name_prefix_len + FRAMEDUMP_FILENAME_EXTRA_BUFSIZE); memcpy(video.framedump.name_prefix, framedump_dir, video.framedump.name_prefix_len); } void video_init(const VideoInitParams *params) { video_init_sdl(); const char *driver = SDL_GetCurrentVideoDriver(); log_info("Using driver '%s'", driver); video_query_capability = video_query_capability_generic; if(!strcmp(driver, "x11")) { video.backend = VIDEO_BACKEND_X11; } else if(!strcmp(driver, "emscripten")) { video.backend = VIDEO_BACKEND_EMSCRIPTEN; video_query_capability = video_query_capability_webcanvas; // We can not start in fullscreen in the browser properly, so disable it here. // Fullscreen is still accessible via the settings menu and the shortcut key. config_set_int(CONFIG_FULLSCREEN, false); } else if(!strcmp(driver, "KMSDRM")) { video.backend = VIDEO_BACKEND_KMSDRM; video_query_capability = video_query_capability_alwaysfullscreen; } else if(!strcmp(driver, "RPI")) { video.backend = VIDEO_BACKEND_RPI; video_query_capability = video_query_capability_alwaysfullscreen; } else if(!strcmp(driver, "Switch")) { video.backend = VIDEO_BACKEND_SWITCH; video_query_capability = video_query_capability_switch; } else { video.backend = VIDEO_BACKEND_OTHER; } if(global.is_kiosk_mode) { video_query_capability_kiosk_fallback = video_query_capability; video_query_capability = video_query_capability_kiosk; } video.scaling_factor = 0; r_init(); int w, h; bool fullscreen; if(params->width > 0 || params->height > 0) { w = params->width; h = params->height; fullscreen = false; if(w <= 0) { w = rint(h * VIDEO_ASPECT_RATIO); } else if(h <= 0) { h = rint(w / VIDEO_ASPECT_RATIO); } } else { w = config_get_int(CONFIG_VID_WIDTH); h = config_get_int(CONFIG_VID_HEIGHT); fullscreen = config_get_int(CONFIG_FULLSCREEN); } video_set_mode( config_get_int(CONFIG_VID_DISPLAY), w, h, fullscreen, config_get_int(CONFIG_VID_RESIZABLE) ); video_update_scaling_factor(); events_register_handler(&(EventHandler) { .proc = video_handle_window_event, .priority = EPRIO_SYSTEM, .event_type = SDL_WINDOWEVENT, }); events_register_handler(&(EventHandler) { .proc = video_handle_config_event, .priority = EPRIO_SYSTEM, .event_type = MAKE_TAISEI_EVENT(TE_CONFIG_UPDATED), }); video_init_framedump(); log_info("Video subsystem initialized"); } void video_post_init(void) { fbmgr_init(); video.postprocess = video_postprocess_init(); r_framebuffer(video_get_screen_framebuffer()); } void video_shutdown(void) { video_postprocess_shutdown(video.postprocess); fbmgr_shutdown(); events_unregister_handler(video_handle_window_event); events_unregister_handler(video_handle_config_event); r_unclaim_window(video.window); SDL_DestroyWindow(video.window); r_shutdown(); dynarray_free_data(&video.win_modes); dynarray_free_data(&video.fs_modes); SDL_VideoQuit(); mem_free(video.framedump.name_prefix); } Framebuffer *video_get_screen_framebuffer(void) { return video_postprocess_get_framebuffer(video.postprocess); } void video_swap_buffers(void) { Framebuffer *pp_fb = video_postprocess_render(video.postprocess); if(pp_fb) { r_flush_sprites(); Framebuffer *prev_fb = r_framebuffer_current(); r_framebuffer(NULL); r_state_push(); r_mat_proj_push_ortho(SCREEN_W, SCREEN_H); r_shader_standard(); r_color3(1, 1, 1); draw_framebuffer_tex(pp_fb, SCREEN_W, SCREEN_H); r_framebuffer_clear(pp_fb, BUFFER_ALL, RGBA(0, 0, 0, 0), 1); r_mat_proj_pop(); r_state_pop(); r_swap(video.window); r_framebuffer(prev_fb); } else { r_swap(video.window); } if(video.framedump.name_prefix) { video_take_framedump(); } // XXX: Unfortunately, there seems to be no reliable way to sync this up with events config_set_int(CONFIG_FULLSCREEN, video_is_fullscreen()); } VideoBackend video_get_backend(void) { return video.backend; } static VideoModeArray *get_vidmode_array(bool fullscreen) { return fullscreen ? &video.fs_modes : &video.win_modes; } bool video_get_mode(uint idx, bool fullscreen, VideoMode *out_mode) { VideoModeArray *a = get_vidmode_array(fullscreen); if(idx >= a->num_elements) { return false; } *out_mode = dynarray_get(a, idx); return true; } uint video_get_num_modes(bool fullscreen) { return get_vidmode_array(fullscreen)->num_elements; } VideoMode video_get_current_mode(void) { return video.current; } double video_get_scaling_factor(void) { return video.scaling_factor; }