taisei/src/video.c

1163 lines
33 KiB
C

/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
*/
#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;
}