/* RetroArch - A frontend for libretro. * Copyright (C) 2010-2014 - Hans-Kristian Arntzen * Copyright (C) 2011-2017 - Daniel De Matteis * Copyright (C) 2021 - David G.F. * * RetroArch is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with RetroArch. * If not, see . */ #include "input/input_driver.h" #include #include #include #include #ifdef HAVE_NETWORKING #include #include #endif #include #include #include #include #include #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef HAVE_CHEEVOS #include "cheevos/cheevos.h" #endif #ifdef HAVE_GFX_WIDGETS #include "gfx/gfx_widgets.h" #endif #ifdef HAVE_MENU #include "menu/menu_driver.h" #endif #ifdef HAVE_NETWORKING #include "network/netplay/netplay.h" #endif #include "audio/audio_driver.h" #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) #include "gfx/video_shader_parse.h" #endif #include "autosave.h" #include "command.h" #include "core_info.h" #include "cheat_manager.h" #include "content.h" #include "dynamic.h" #include "list_special.h" #include "paths.h" #include "retroarch.h" #include "runloop.h" #include "verbosity.h" #include "version.h" #include "version_git.h" #include "tasks/task_content.h" #define CMD_BUF_SIZE 4096 static void command_post_state_loaded(void) { #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) { const char *_msg = msg_hash_to_str(MSG_CHEEVOS_HARDCORE_MODE_DISABLED); rcheevos_pause_hardcore(); runloop_msg_queue_push(_msg, strlen(_msg), 0, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } #endif #ifdef HAVE_NETWORKING netplay_driver_ctl(RARCH_NETPLAY_CTL_LOAD_SAVESTATE, NULL); #endif { video_driver_state_t *video_st = video_state_get_ptr(); bool frame_time_counter_auto_reset = config_get_ptr()->bools.frame_time_counter_auto_reset; if (frame_time_counter_auto_reset) video_st->frame_time_count = 0; } #if defined(HAVE_GFX_WIDGETS) && defined(HAVE_SCREENSHOTS) { gfx_widget_state_slot_show(dispwidget_get_ptr(), NULL, NULL); } #endif } #if defined(HAVE_COMMAND) /* Generic command parse utilities */ static bool command_get_arg(const char *tok, const char **arg, unsigned *index) { unsigned i; for (i = 0; i < ARRAY_SIZE(map); i++) { if (string_is_equal(tok, map[i].str)) { if (arg) *arg = NULL; if (index) *index = i; return true; } } for (i = 0; i < ARRAY_SIZE(action_map); i++) { const char *str = strstr(tok, action_map[i].str); if (str == tok) { const char *argument = str + strlen(action_map[i].str); if (!argument) return false; if (*argument != ' ' && *argument != '\0') return false; if (arg) *arg = argument + 1; if (index) *index = i; return true; } } return false; } static void command_parse_sub_msg(command_t *handle, const char *tok) { const char *arg = NULL; unsigned index = 0; if (command_get_arg(tok, &arg, &index)) { if (arg) { if (!action_map[index].action(handle, arg)) RARCH_ERR("[Command] Command \"%s\" failed.\n", arg); } else { /* For MENU_TOGGLE, bypass the press-and-release mechanism * by directly invoking the command event. This avoids * timing issues with runahead (single-instance) where the * 1-frame pulse from network commands can be lost. */ if (map[index].id == RARCH_MENU_TOGGLE) command_event(CMD_EVENT_MENU_TOGGLE, NULL); else handle->state[map[index].id] = true; } } else RARCH_WARN(msg_hash_to_str(MSG_UNRECOGNIZED_COMMAND), tok); } static void command_parse_msg(command_t *handle, char *buf) { char *save = NULL; const char *tok = strtok_r(buf, "\n", &save); while (tok) { command_parse_sub_msg(handle, tok); tok = strtok_r(NULL, "\n", &save); } } #if defined(HAVE_NETWORK_CMD) typedef struct { /* Network socket FD */ int net_fd; /* Source address for the command received */ struct sockaddr_storage cmd_source; /* Size of the previous structure in use */ socklen_t cmd_source_len; } command_network_t; static void network_command_reply(command_t *cmd, const char *s, size_t len) { command_network_t *netcmd = (command_network_t*)cmd->userptr; /* Respond (fire and forget since it's UDP) */ sendto(netcmd->net_fd, s, len, 0, (struct sockaddr*)&netcmd->cmd_source, netcmd->cmd_source_len); } static void network_command_free(command_t *handle) { command_network_t *netcmd = (command_network_t*)handle->userptr; if (netcmd->net_fd >= 0) socket_close(netcmd->net_fd); free(netcmd); free(handle); } static void command_network_poll(command_t *handle) { ssize_t ret; char buf[2048]; command_network_t *netcmd = (command_network_t*)handle->userptr; if (netcmd->net_fd < 0) return; for (;;) { netcmd->cmd_source_len = sizeof(netcmd->cmd_source); if ((ret = recvfrom(netcmd->net_fd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&netcmd->cmd_source, &netcmd->cmd_source_len)) <= 0) return; buf[ret] = '\0'; command_parse_msg(handle, buf); } } command_t* command_network_new(uint16_t port) { struct addrinfo *res = NULL; command_t *cmd = NULL; command_network_t *netcmd = NULL; int fd = -1; /* Allocate-then-check in sequence, matching command_uds_new. * The previous code allocated cmd + netcmd back-to-back and then * assigned netcmd->net_fd / cmd->userptr before NULL-checking * either pointer - an OOM on either calloc would NULL-deref on * line 252/253. It also leaked netcmd on the '!cmd' failure * path via the 'error' label (which now cleanly frees both * because they are initialised to NULL up-front; free(NULL) is * a no-op). */ if (!(cmd = (command_t*)calloc(1, sizeof(*cmd)))) goto error; if (!(netcmd = (command_network_t*)calloc(1, sizeof(command_network_t)))) goto error; fd = socket_init((void**)&res, port, NULL, SOCKET_TYPE_DATAGRAM, AF_INET); RARCH_LOG("[NetCMD] %s %hu.\n", msg_hash_to_str(MSG_BRINGING_UP_COMMAND_INTERFACE_ON_PORT), (unsigned short)port); if (fd < 0) goto error; netcmd->net_fd = fd; cmd->userptr = netcmd; cmd->poll = command_network_poll; cmd->replier = network_command_reply; cmd->destroy = network_command_free; if (!socket_nonblock(netcmd->net_fd)) goto error; if (!socket_bind(netcmd->net_fd, (void*)res)) { RARCH_ERR("[NetCMD] %s\n", msg_hash_to_str(MSG_FAILED_TO_BIND_SOCKET)); goto error; } freeaddrinfo_retro(res); return cmd; error: if (res) freeaddrinfo_retro(res); /* The only path that reaches 'error' with fd >= 0 is the * socket_nonblock / socket_bind failure after fd was already * stored in netcmd->net_fd. Close it so we don't leak the * socket file descriptor along with the allocations. */ if (fd >= 0) socket_close(fd); free(netcmd); free(cmd); return NULL; } #endif #if defined(HAVE_STDIN_CMD) typedef struct { /* Buffer and pointer for stdin reads */ size_t stdin_buf_ptr; char stdin_buf[CMD_BUF_SIZE]; } command_stdin_t; static void stdin_command_reply(command_t *cmd, const char *s, size_t len) { /* Just write to stdout! */ fwrite(s, 1, len, stdout); fflush(stdout); } static void stdin_command_free(command_t *handle) { free(handle->userptr); free(handle); } static void command_stdin_poll(command_t *handle) { command_stdin_t *stdincmd = (command_stdin_t*)handle->userptr; ssize_t ret = read_stdin( stdincmd->stdin_buf + stdincmd->stdin_buf_ptr, CMD_BUF_SIZE - stdincmd->stdin_buf_ptr - 1); if (ret != 0) { char *last_newline = NULL; stdincmd->stdin_buf_ptr += ret; /* Ensure we don't write past buffer bounds */ if (stdincmd->stdin_buf_ptr >= CMD_BUF_SIZE) stdincmd->stdin_buf_ptr = CMD_BUF_SIZE - 1; stdincmd->stdin_buf[stdincmd->stdin_buf_ptr] = '\0'; last_newline = strrchr(stdincmd->stdin_buf, '\n'); if (!last_newline) { /* We're receiving bogus data in pipe * (no terminating newline), flush out the buffer. */ if (stdincmd->stdin_buf_ptr + 1 >= CMD_BUF_SIZE) { stdincmd->stdin_buf_ptr = 0; stdincmd->stdin_buf[0] = '\0'; } } else { ptrdiff_t msg_len; *last_newline++ = '\0'; msg_len = last_newline - stdincmd->stdin_buf; command_parse_msg(handle, stdincmd->stdin_buf); memmove(stdincmd->stdin_buf, last_newline, stdincmd->stdin_buf_ptr - msg_len); stdincmd->stdin_buf_ptr -= msg_len; } } } command_t* command_stdin_new(void) { command_t *cmd; command_stdin_t *stdincmd; #if !(defined(_WIN32) || defined(EMSCRIPTEN)) #ifdef HAVE_NETWORKING if (!socket_nonblock(STDIN_FILENO)) return NULL; #endif #endif /* Allocate in order with per-step failure handling. The earlier * form allocated both cmd and stdincmd back-to-back then checked * them, which leaked stdincmd on the '!cmd' failure path (the * '!cmd' return simply dropped the stdincmd pointer). Also * matches the command_uds_new pattern further down. */ if (!(cmd = (command_t*)calloc(1, sizeof(command_t)))) return NULL; if (!(stdincmd = (command_stdin_t*)calloc(1, sizeof(command_stdin_t)))) { free(cmd); return NULL; } cmd->userptr = stdincmd; cmd->poll = command_stdin_poll; cmd->replier = stdin_command_reply; cmd->destroy = stdin_command_free; return cmd; } #endif #if defined(EMSCRIPTEN) #include "frontend/drivers/platform_emscripten.h" typedef struct { char command_buf[CMD_BUF_SIZE]; } command_emscripten_t; static void emscripten_command_reply(command_t *_cmd, const char *s, size_t len) { platform_emscripten_command_reply(s, len); } static void emscripten_command_free(command_t *handle) { free(handle->userptr); free(handle); } static void command_emscripten_poll(command_t *handle) { command_emscripten_t *emscriptencmd = (command_emscripten_t*)handle->userptr; ptrdiff_t msg_len = platform_emscripten_command_read((char **)(&emscriptencmd->command_buf), CMD_BUF_SIZE); if (msg_len != 0) command_parse_msg(handle, emscriptencmd->command_buf); } command_t* command_emscripten_new(void) { command_t *cmd; command_emscripten_t *emscriptencmd; /* Same sequential-allocation-with-per-step-cleanup pattern as * command_stdin_new / command_uds_new. Previously allocated both * structs before any NULL check, leaking emscriptencmd on the * '!cmd' failure path. */ if (!(cmd = (command_t*)calloc(1, sizeof(command_t)))) return NULL; if (!(emscriptencmd = (command_emscripten_t*)calloc(1, sizeof(command_emscripten_t)))) { free(cmd); return NULL; } cmd->userptr = emscriptencmd; cmd->poll = command_emscripten_poll; cmd->replier = emscripten_command_reply; cmd->destroy = emscripten_command_free; return cmd; } #endif bool command_get_config_param(command_t *cmd, const char* arg) { size_t _len; char reply[8192]; #ifdef HAVE_BSV_MOVIE char value_dynamic[256]; #endif const char *value = "unsupported"; settings_t *settings = config_get_ptr(); bool video_fullscreen = settings->bools.video_fullscreen; const char *dir_runtime_log = settings->paths.directory_runtime_log; const char *log_dir = settings->paths.log_dir; const char *directory_cache = settings->paths.directory_cache; const char *directory_system = settings->paths.directory_system; const char *path_username = settings->paths.username; if (memcmp(arg, "video_fullscreen", sizeof("video_fullscreen")) == 0) { if (video_fullscreen) value = "true"; else value = "false"; } else if (memcmp(arg, "savefile_directory", sizeof("savefile_directory")) == 0) value = dir_get_ptr(RARCH_DIR_SAVEFILE); else if (memcmp(arg, "savestate_directory", sizeof("savestate_directory")) == 0) value = dir_get_ptr(RARCH_DIR_SAVESTATE); else if (memcmp(arg, "runtime_log_directory", sizeof("runtime_log_directory")) == 0) value = dir_runtime_log; else if (memcmp(arg, "log_dir", sizeof("log_dir")) == 0) value = log_dir; else if (memcmp(arg, "cache_directory", sizeof("cache_directory")) == 0) value = directory_cache; else if (memcmp(arg, "system_directory", sizeof("system_directory")) == 0) value = directory_system; else if (memcmp(arg, "netplay_nickname", sizeof("netplay_nickname")) == 0) value = path_username; #ifdef HAVE_BSV_MOVIE else if (memcmp(arg, "active_replay", sizeof("active_replay")) == 0) { input_driver_state_t *input_st = input_state_get_ptr(); value = value_dynamic; value_dynamic[0] = '\0'; if (input_st->bsv_movie_state_handle) { bsv_movie_t *movie = input_st->bsv_movie_state_handle; snprintf(value_dynamic, sizeof(value_dynamic), "%lld %u %lld", (long long)(movie->identifier), input_st->bsv_movie_state.flags, (long long)(movie->frame_counter)); } else strlcpy(value_dynamic, "0 0 0", sizeof(value_dynamic)); } #endif #ifdef HAVE_MENU else if (memcmp(arg, "menu_active", sizeof("menu_active")) == 0) { struct menu_state* menu_st = menu_state_get_ptr(); if (menu_st && (menu_st->flags & MENU_ST_FLAG_ALIVE)) value = "true"; else value = "false"; } #endif #ifdef HAVE_CHEEVOS else if (memcmp(arg, "cheevos_enable", sizeof("cheevos_enable")) == 0) { if (settings->bools.cheevos_enable) value = "true"; else value = "false"; } #endif /* TODO: query any string */ _len = strlcpy(reply, "GET_CONFIG_PARAM ", sizeof(reply)); _len += strlcpy(reply + _len, arg, sizeof(reply) - _len); reply[ _len] = ' '; reply[++_len] = '\0'; _len += strlcpy(reply + _len, value, sizeof(reply) - _len); cmd->replier(cmd, reply, _len); return true; } #if defined(HAVE_LAKKA) #include #define MAX_USER_CONNECTIONS 4 typedef struct { /* File descriptor for the domain socket */ int sfd; /* Client sockets */ int userfd[MAX_USER_CONNECTIONS]; /* Last received user socket */ int last_fd; } command_uds_t; static void uds_command_reply(command_t *cmd, const char *s, size_t len) { command_uds_t *subcmd = (command_uds_t*)cmd->userptr; write(subcmd->last_fd, s, len); } static void uds_command_free(command_t *handle) { int i; command_uds_t *udscmd = (command_uds_t*)handle->userptr; for (i = 0; i < MAX_USER_CONNECTIONS; i++) if (udscmd->userfd[i] >= 0) socket_close(udscmd->userfd[i]); socket_close(udscmd->sfd); free(handle->userptr); free(handle); } static void command_uds_poll(command_t *handle) { int i; int fd; ssize_t ret; char buf[2048]; command_uds_t *udscmd = (command_uds_t*)handle->userptr; if (udscmd->sfd < 0) return; /* Read data from clients and process commands */ for (i = 0; i < MAX_USER_CONNECTIONS; i++) { bool err = false; fd = udscmd->userfd[i]; if (fd < 0) continue; ret = socket_receive_all_nonblocking(fd, &err, buf, sizeof(buf) - 1); if (!ret) continue; if (!err) { buf[ret] = '\0'; udscmd->last_fd = fd; command_parse_msg(handle, buf); } else { socket_close(fd); udscmd->userfd[i] = -1; } } /* Accepts new connections from clients */ fd = accept(udscmd->sfd, NULL, NULL); if (fd >= 0) { if (socket_nonblock(fd)) { for (i = 0; i < MAX_USER_CONNECTIONS; i++) { if (udscmd->userfd[i] < 0) { udscmd->userfd[i] = fd; return; } } } socket_close(fd); } } command_t* command_uds_new(void) { int i; command_t *cmd; command_uds_t *subcmd; struct sockaddr_un addr; socklen_t addrsz = offsetof(struct sockaddr_un, sun_path) + STRLEN_CONST("retroarch/cmd") + 1; int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return NULL; /* use an abstract socket for simplicity */ memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strlcpy(&addr.sun_path[1], "retroarch/cmd", sizeof(addr.sun_path) - 1); if ( bind(fd, (struct sockaddr*)&addr, addrsz) < 0 || listen(fd, MAX_USER_CONNECTIONS) < 0 || !socket_nonblock(fd)) { socket_close(fd); return NULL; } cmd = (command_t*)calloc(1, sizeof(command_t)); if (!cmd) { socket_close(fd); return NULL; } subcmd = (command_uds_t*)calloc(1, sizeof(command_uds_t)); if (!subcmd) { free(cmd); socket_close(fd); return NULL; } subcmd->sfd = fd; subcmd->last_fd = -1; for (i = 0; i < MAX_USER_CONNECTIONS; i++) subcmd->userfd[i] = -1; cmd->userptr = subcmd; cmd->poll = command_uds_poll; cmd->replier = uds_command_reply; cmd->destroy = uds_command_free; return cmd; } #endif /* Routines used to invoke retroarch command ... */ #ifdef HAVE_NETWORK_CMD static bool command_verify(const char *cmd) { size_t i; if (command_get_arg(cmd, NULL, NULL)) return true; RARCH_ERR("[NetCMD] Command \"%s\" is not recognized by the program.\n", cmd); RARCH_ERR("[NetCMD] \tValid commands:\n"); for (i = 0; i < ARRAY_SIZE(map); i++) RARCH_ERR("[NetCMD] \t\t%s\n", map[i].str); for (i = 0; i < ARRAY_SIZE(action_map); i++) RARCH_ERR("[NetCMD] \t\t%s %s\n", action_map[i].str, action_map[i].arg_desc); return false; } static bool udp_send_packet(const char *host, uint16_t port, const char *msg) { char port_buf[6]; const struct addrinfo *tmp_info; struct addrinfo *addr = NULL; struct addrinfo hints = {0}; size_t len = strlen(msg); bool ret = false; snprintf(port_buf, sizeof(port_buf), "%hu", (unsigned short)port); hints.ai_socktype = SOCK_DGRAM; hints.ai_flags = AI_NUMERICSERV; if (getaddrinfo_retro(host, port_buf, &hints, &addr)) return false; if (!addr) return false; /* Send to all possible targets. */ tmp_info = addr; do { int fd = socket(tmp_info->ai_family, tmp_info->ai_socktype, tmp_info->ai_protocol); if (fd < 0) continue; if (sendto(fd, msg, len, 0, tmp_info->ai_addr, tmp_info->ai_addrlen) == (ssize_t)len) ret = true; socket_close(fd); } while ((tmp_info = tmp_info->ai_next)); freeaddrinfo_retro(addr); return ret; } bool command_network_send(const char *cmd_) { char buf[4096]; char *ptr; const char *cmd = NULL; const char *host = NULL; const char *port_str = NULL; uint16_t port = DEFAULT_NETWORK_CMD_PORT; size_t len; if (!cmd_ || !*cmd_) return false; len = strlen(cmd_); if (len >= sizeof(buf)) return false; if (!network_init()) return false; memcpy(buf, cmd_, len + 1); cmd = buf; ptr = strchr(buf, ';'); if (ptr) { *ptr++ = '\0'; host = ptr; ptr = strchr(ptr, ';'); if (ptr) { *ptr++ = '\0'; port_str = ptr; } } if (!cmd || !*cmd) return false; if (!host || !*host) { #ifdef _WIN32 host = "127.0.0.1"; #else host = "localhost"; #endif } if (port_str && *port_str) { unsigned long val = strtoul(port_str, NULL, 0); if (val > 0 && val <= 0xFFFF) port = (uint16_t)val; } RARCH_LOG("[NetCMD] %s: \"%s\" to %s:%hu.\n", msg_hash_to_str(MSG_SENDING_COMMAND), cmd, host, (unsigned short)port); if (!command_verify(cmd)) return false; return udp_send_packet(host, port, cmd); } #endif bool command_show_osd_msg(command_t *cmd, const char* arg) { runloop_msg_queue_push(arg, strlen(arg), 1, 180, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); return true; } bool command_load_state_slot(command_t *cmd, const char *arg) { char state_path[PATH_MAX_LENGTH] = ""; size_t _len = 0; char reply[128] = ""; unsigned int slot = (unsigned int)strtoul(arg, NULL, 10); bool savestates_enabled = core_info_current_supports_savestate(); bool ret = false; _len = strlcpy(reply, "LOAD_STATE_SLOT ", sizeof(reply)); _len += snprintf(reply + _len, sizeof(reply) - _len, "%d", slot); runloop_get_savestate_path(state_path, sizeof(state_path), slot); /* For LOADING, an existing state file outranks metadata and * save-capability probes: core_serialize_size() measures whether the * core can SAVE right now (0 at e.g. a game's own main menu), which * says nothing about whether it can restore. Let the load task and * retro_unserialize() arbitrate. */ if (!savestates_enabled) savestates_enabled = path_is_valid(state_path); if (savestates_enabled) { if ((ret = content_load_state(state_path, false, false))) command_post_state_loaded(); } else ret = false; cmd->replier(cmd, reply, _len); return ret; } bool command_save_state_slot(command_t* cmd, const char* arg) { char state_path[PATH_MAX_LENGTH] = ""; size_t _len = 0; char reply[128] = ""; unsigned int slot = (unsigned int)strtoul(arg, NULL, 10); bool savestates_enabled = core_info_current_supports_savestate(); bool ret = false; _len = strlcpy(reply, "SAVE_STATE_SLOT ", sizeof(reply)); _len += snprintf(reply + _len, sizeof(reply) - _len, "%d", slot); if (savestates_enabled) { size_t info_size; runloop_get_savestate_path(state_path, sizeof(state_path), slot); info_size = core_serialize_size(); savestates_enabled = (info_size > 0); } if (savestates_enabled) ret = content_save_state(state_path, true); else ret = false; cmd->replier(cmd, reply, _len); return ret; } bool command_play_replay_slot(command_t *cmd, const char *arg) { #ifdef HAVE_BSV_MOVIE char replay_path[16384]; char reply[128] = ""; unsigned int slot = (unsigned int)strtoul(arg, NULL, 10); bool savestates_enabled = core_info_current_supports_savestate(); bool ret = false; replay_path[0] = '\0'; if (savestates_enabled) { size_t info_size; runloop_get_replay_path(replay_path, sizeof(replay_path), slot); info_size = core_serialize_size(); savestates_enabled = (info_size > 0); } if (savestates_enabled) { ret = movie_start_playback(input_state_get_ptr(), replay_path); if (ret) { input_driver_state_t *input_st = input_state_get_ptr(); task_queue_wait(NULL, NULL); if (input_st->bsv_movie_state_next_handle) snprintf(reply, sizeof(reply) - 1, "PLAY_REPLAY_SLOT %lld", (long long)(input_st->bsv_movie_state_next_handle->identifier)); else snprintf(reply, sizeof(reply) - 1, "PLAY_REPLAY_SLOT 0"); command_post_state_loaded(); } } else ret = false; cmd->replier(cmd, reply, strlen(reply)); return ret; #else return false; #endif } bool command_seek_replay(command_t *cmd, const char *arg) { #ifdef HAVE_BSV_MOVIE char reply[32]; char *endptr; size_t _len; bool ret = true; int64_t frame = strtoll(arg, &endptr, 10); input_driver_state_t *input_st = input_state_get_ptr(); if (!endptr) ret = false; if (!(input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_PLAYBACK | BSV_FLAG_MOVIE_RECORDING))) ret = false; #ifdef HAVE_CHEEVOS ret = !rcheevos_hardcore_active(); #endif if (ret) ret = movie_seek_to_frame(input_st, frame); if (ret) { _len = strlcpy(reply, "OK ", sizeof(reply)); _len += snprintf(reply+_len, sizeof(reply)-_len, "%" PRId64, input_st->bsv_movie_state.seek_target_frame); } else _len = strlcpy(reply, "NO", sizeof(reply)); reply[_len] = '\n'; reply[++_len] = '\0'; cmd->replier(cmd, reply, _len); return ret; #else cmd->replier(cmd, "NO\n", 4); return false; #endif } bool command_save_savefiles(command_t *cmd, const char* arg) { char reply[4]; bool ret; size_t _len = strlcpy(reply, "OK", sizeof(reply)); reply[ _len] = '\n'; reply[++_len] = '\0'; /* In the future, this should probably send each saved file path to the replier. */ ret = command_event(CMD_EVENT_SAVE_FILES, NULL); if (!ret) strlcpy(reply, "NO", sizeof(reply)); cmd->replier(cmd, reply, _len); return ret; } bool command_load_savefiles(command_t *cmd, const char* arg) { char reply[4]; bool ret; size_t _len = strlcpy(reply, "OK", sizeof(reply)); reply[ _len] = '\n'; reply[++_len] = '\0'; ret = command_event(CMD_EVENT_LOAD_FILES, NULL); if (!ret) strlcpy(reply, "NO", sizeof(reply)); cmd->replier(cmd, reply, _len); return ret; } #if defined(HAVE_CHEEVOS) bool command_read_ram(command_t *cmd, const char *arg) { unsigned int nbytes = 0; unsigned int addr = -1; char *end = NULL; addr = (unsigned int)strtoul(arg, &end, 16); if (end && *end == ' ') nbytes = (unsigned int)strtoul(end + 1, NULL, 10); if (end && *end == ' ' && nbytes > 0) { size_t _len = 0; char *reply_at = NULL; const uint8_t *data = NULL; /* We allocate more than needed, saving 20 bytes is not really relevant */ unsigned int alloc_size = 40 + nbytes * 3; char *reply = (char*)malloc(alloc_size); if (!reply) { cmd->replier(cmd, "READ_CORE_RAM ERROR: OUT OF MEMORY\n", 34); return true; } reply[0] = '\0'; reply_at = reply + snprintf( reply, alloc_size - 1, "READ_CORE_RAM" " %x", addr); if ((data = rcheevos_patch_address(addr))) { size_t i; for (i = 0; i < nbytes; i++) snprintf(reply_at + 3 * i, 4, " %.2X", data[i]); reply_at[3 * nbytes] = '\n'; _len = reply_at + 3 * nbytes + 1 - reply; } else { strlcpy(reply_at, " -1\n", sizeof(reply) - strlen(reply)); _len = reply_at + STRLEN_CONST(" -1\n") - reply; } cmd->replier(cmd, reply, _len); free(reply); } return true; } bool command_write_ram(command_t *cmd, const char *arg) { unsigned int addr = (unsigned int)strtoul(arg, (char**)&arg, 16); uint8_t *data = (uint8_t *)rcheevos_patch_address(addr); if (!data) return false; if (rcheevos_hardcore_active()) { RARCH_LOG("[Command] Achievements hardcore mode disabled by WRITE_CORE_RAM.\n"); rcheevos_pause_hardcore(); } while (*arg) { *data = strtoul(arg, (char**)&arg, 16); data++; } return true; } #endif bool command_version(command_t *cmd, const char* arg) { char reply[256]; size_t _len = strlcpy(reply, PACKAGE_VERSION, sizeof(reply)); reply[ _len] = '\n'; reply[++_len] = '\0'; cmd->replier(cmd, reply, _len); return true; } bool command_load_core(command_t *cmd, const char* arg) { content_ctx_info_t content_info = {0}; task_push_load_new_core(arg, NULL, &content_info, CORE_TYPE_PLAIN, NULL, NULL); return true; } static const rarch_memory_descriptor_t* command_memory_get_descriptor(const rarch_memory_map_t* mmap, unsigned address, size_t* offset) { const rarch_memory_descriptor_t* desc = mmap->descriptors; const rarch_memory_descriptor_t* end = desc + mmap->num_descriptors; for (; desc < end; desc++) { if (desc->core.select == 0) { /* if select is 0, attempt to explicitly match the address */ if (address >= desc->core.start && address < desc->core.start + desc->core.len) { *offset = address - desc->core.start; return desc; } } else { /* otherwise, attempt to match the address by matching the select bits */ if (((desc->core.start ^ address) & desc->core.select) == 0) { /* adjust the address to the start of the descriptor */ unsigned desc_offset = address - (unsigned)desc->core.start; /* address is unsigned. we only need that much of the disconnect mask */ unsigned mask = (unsigned)desc->core.disconnect; /* this magic logic is copied from mmap_reduce. it removes any bits from * address that are non-zero in the disconnect field. bits above the * removed bits are shifted down to fill the gap. */ while (mask) { const unsigned tmp = (mask - 1) & ~mask; desc_offset = (desc_offset & tmp) | ((desc_offset >> 1) & ~tmp); mask = (mask & (mask - 1)) >> 1; } /* we've calculated the actual offset of the data within the descriptor */ *offset = desc_offset; /* sanity check - make sure the descriptor is large enough to hold the target address */ if (desc_offset < desc->core.len) return desc; } } } return NULL; } static uint8_t *command_memory_get_pointer( const rarch_system_info_t* sys_info, unsigned address, unsigned int* max_bytes, int for_write, char *s, size_t len) { if (!sys_info || sys_info->mmaps.num_descriptors == 0) strlcpy(s, " -1 no memory map defined\n", len); else { size_t offset; const rarch_memory_descriptor_t* desc = command_memory_get_descriptor(&sys_info->mmaps, address, &offset); if (!desc) strlcpy(s, " -1 no descriptor for address\n", len); else if (!desc->core.ptr) strlcpy(s, " -1 no data for descriptor\n", len); else if (for_write && (desc->core.flags & RETRO_MEMDESC_CONST)) strlcpy(s, " -1 descriptor data is readonly\n", len); else { *max_bytes = (unsigned int)(desc->core.len - offset); return (uint8_t*)desc->core.ptr + desc->core.offset + offset; } } *max_bytes = 0; return NULL; } bool command_get_status(command_t *cmd, const char* arg) { size_t _len; char reply[4096]; uint8_t flags; if (!cmd) return false; if (!cmd->replier) return false; flags = content_get_flags(); if (flags & CONTENT_ST_FLAG_IS_INITED) { /* add some content info */ core_info_t *core_info = NULL; runloop_state_t *runloop_st = runloop_state_get_ptr(); const char *basename_path = NULL; if (!runloop_st) { _len = strlcpy(reply, "GET_STATUS ERROR", sizeof(reply)); cmd->replier(cmd, reply, _len); return false; } core_info_get_current_core(&core_info); _len = 0; strlcpy_append(reply, sizeof(reply), &_len, "GET_STATUS "); if (runloop_st->flags & RUNLOOP_FLAG_PAUSED) strlcpy_append(reply, sizeof(reply), &_len, "PAUSED"); else strlcpy_append(reply, sizeof(reply), &_len, "PLAYING"); strlcpy_append(reply, sizeof(reply), &_len, " "); if (core_info && core_info->system_id) strlcpy_append(reply, sizeof(reply), &_len, core_info->system_id); else if (runloop_st->system.info.library_name) strlcpy_append(reply, sizeof(reply), &_len, runloop_st->system.info.library_name); else strlcpy_append(reply, sizeof(reply), &_len, "UNKNOWN"); strlcpy_append(reply, sizeof(reply), &_len, ","); basename_path = path_get(RARCH_PATH_BASENAME); if (basename_path) { const char *basename = path_basename(basename_path); if (basename) strlcpy_append(reply, sizeof(reply), &_len, basename); else strlcpy_append(reply, sizeof(reply), &_len, "UNKNOWN"); } else strlcpy_append(reply, sizeof(reply), &_len, "UNKNOWN"); _len += snprintf(reply + _len, sizeof(reply) - _len, ",crc32=%lx\n", (unsigned long)content_get_crc()); } else _len = strlcpy(reply, "GET_STATUS CONTENTLESS", sizeof(reply)); cmd->replier(cmd, reply, _len); return true; } bool command_read_memory(command_t *cmd, const char *arg) { unsigned i; char* reply = NULL; char* reply_at = NULL; const uint8_t* data = NULL; unsigned int nbytes = 0; unsigned int alloc_size = 0; unsigned int address = -1; size_t _len = 0; unsigned int max_bytes = 0; runloop_state_t *runloop_st = runloop_state_get_ptr(); const rarch_system_info_t* sys_info= &runloop_st->system; { char *end = NULL; address = (unsigned int)strtoul(arg, &end, 16); if (!(end && *end == ' ')) return false; nbytes = (unsigned int)strtoul(end + 1, NULL, 10); if (nbytes == 0) return false; } /* Ensure large enough to return all requested bytes or an error message */ alloc_size = 64 + nbytes * 3; reply = (char*)malloc(alloc_size); if (!reply) { cmd->replier(cmd, "READ_CORE_MEMORY ERROR: OUT OF MEMORY\n", 37); return true; } reply_at = reply + snprintf(reply, alloc_size - 1, "READ_CORE_MEMORY %x", address); if ((data = command_memory_get_pointer( sys_info, address, &max_bytes, 0, reply_at, alloc_size - strlen(reply)))) { if (nbytes > max_bytes) nbytes = max_bytes; for (i = 0; i < nbytes; i++) snprintf(reply_at + 3 * i, 4, " %02X", data[i]); reply_at[3 * nbytes] = '\n'; _len = reply_at + 3 * nbytes + 1 - reply; } else _len = strlen(reply); cmd->replier(cmd, reply, _len); free(reply); return true; } bool command_write_memory(command_t *cmd, const char *arg) { unsigned int address = (unsigned int)strtoul(arg, (char**)&arg, 16); unsigned int max_bytes = 0; char reply[128] = ""; runloop_state_t *runloop_st = runloop_state_get_ptr(); const rarch_system_info_t *sys_info = &runloop_st->system; char *reply_at = reply + snprintf(reply, sizeof(reply) - 1, "WRITE_CORE_MEMORY %x", address); uint8_t *data = command_memory_get_pointer(sys_info, address, &max_bytes, 1, reply_at, sizeof(reply) - strlen(reply) - 1); if (data) { uint8_t* start = data; while (*arg && max_bytes > 0) { --max_bytes; *data = strtoul(arg, (char**)&arg, 16); data++; } snprintf(reply_at, sizeof(reply) - strlen(reply) - 1, " %u\n", (unsigned)(data - start)); #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) { RARCH_LOG("[Command] Achievements hardcore mode disabled by WRITE_CORE_MEMORY.\n"); rcheevos_pause_hardcore(); } #endif } cmd->replier(cmd, reply, strlen(reply)); return true; } #endif void command_event_set_volume( settings_t *settings, float gain, bool widgets_active, bool audio_driver_mute_enable) { size_t _len; char msg[128]; float new_volume = settings->floats.audio_volume + gain; new_volume = MAX(new_volume, -80.0f); new_volume = MIN(new_volume, 12.0f); configuration_set_float(settings, settings->floats.audio_volume, new_volume); _len = strlcpy(msg, msg_hash_to_str(MSG_AUDIO_VOLUME), sizeof(msg)); _len += strlcpy(msg + _len, ": ", sizeof(msg) - _len); _len += snprintf(msg + _len, sizeof(msg) - _len, "%.1f", new_volume); _len += strlcpy(msg + _len, " dB", sizeof(msg) - _len); #if defined(HAVE_GFX_WIDGETS) if (widgets_active) gfx_widget_volume_update_and_show(new_volume, audio_driver_mute_enable); else #endif runloop_msg_queue_push(msg, _len, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_LOG("[Audio] %s.\n", msg); audio_set_float(AUDIO_ACTION_VOLUME_GAIN, new_volume); } /** * event_set_mixer_volume: * @gain : amount of gain to be applied to current volume level. * * Adjusts the current audio volume level. * **/ void command_event_set_mixer_volume( settings_t *settings, float gain) { size_t _len; char msg[128]; float new_volume = settings->floats.audio_mixer_volume + gain; new_volume = MAX(new_volume, -80.0f); new_volume = MIN(new_volume, 12.0f); configuration_set_float(settings, settings->floats.audio_mixer_volume, new_volume); _len = strlcpy(msg, msg_hash_to_str(MSG_AUDIO_VOLUME), sizeof(msg)); _len += strlcpy(msg + _len, ": ", sizeof(msg) - _len); _len += snprintf(msg + _len, sizeof(msg) - _len, "%.1f", new_volume); _len += strlcpy(msg + _len, " dB", sizeof(msg) - _len); runloop_msg_queue_push(msg, _len, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_LOG("[Audio] %s.\n", msg); audio_set_float(AUDIO_ACTION_VOLUME_GAIN, new_volume); } void command_event_init_controllers(rarch_system_info_t *sys_info, settings_t *settings, unsigned num_active_users) { unsigned port; unsigned num_core_ports = sys_info->ports.size; for (port = 0; port < num_core_ports; port++) { size_t i; retro_ctx_controller_info_t pad; unsigned device = RETRO_DEVICE_NONE; const struct retro_controller_description *desc = NULL; /* Check whether current core port is mapped * to an input device * > If is not, leave 'device' set to * 'RETRO_DEVICE_NONE' * > For example: if input ports 0 and 1 are * mapped to core port 0, core port 1 will * be unmapped and should be disabled */ for (i = 0; i < num_active_users; i++) { if (i >= MAX_USERS) break; if (port == settings->uints.input_remap_ports[i]) { device = input_config_get_device(port); break; } } desc = libretro_find_controller_description( &sys_info->ports.data[port], device); if (desc && !desc->desc) { /* If we're trying to connect a completely unknown device, * revert back to JOYPAD. */ if (device != RETRO_DEVICE_JOYPAD && device != RETRO_DEVICE_NONE) { /* Do not fix device, * because any use of dummy core will reset this, * which is not a good idea. */ RARCH_WARN("[Input] Input device ID %u is unknown to this " "libretro implementation. Using RETRO_DEVICE_JOYPAD.\n", device); device = RETRO_DEVICE_JOYPAD; } } pad.device = device; pad.port = port; core_set_controller_port_device(&pad); } } #ifdef HAVE_CONFIGFILE static size_t command_event_save_config(const char *config_path, char *s, size_t len) { size_t _len = 0; bool path_exists = *config_path; const char *str = path_exists ? config_path : path_get(RARCH_PATH_CONFIG); /* Workaround for libdecor 0.2.0 setting unwanted locale */ #if defined(HAVE_WAYLAND) && defined(HAVE_DYNAMIC) setlocale(LC_NUMERIC,"C"); #endif if (path_exists && config_save_file(config_path)) { #if IOS char tmp[PATH_MAX_LENGTH] = {0}; fill_pathname_abbreviate_special(tmp, config_path, sizeof(tmp)); _len = snprintf(s, len, "%s \"%s\".", msg_hash_to_str(MSG_SAVED_NEW_CONFIG_TO), tmp); #else _len = snprintf(s, len, "%s \"%s\".", msg_hash_to_str(MSG_SAVED_NEW_CONFIG_TO), config_path); #endif RARCH_LOG("[Config] %s\n", s); return _len; } if (str && *str) { _len = snprintf(s, len, "%s \"%s\".", msg_hash_to_str(MSG_FAILED_SAVING_CONFIG_TO), str); RARCH_ERR("[Config] %s\n", s); } return _len; } #endif static size_t command_event_undo_save_state(char *s, size_t len) { if (content_undo_save_buf_is_empty()) { enum msg_hash_enums msg = content_undo_save_disabled() ? MSG_CORE_DOES_NOT_SUPPORT_SAVESTATE_UNDO : MSG_NO_SAVE_STATE_HAS_BEEN_OVERWRITTEN_YET; return strlcpy(s, msg_hash_to_str(msg), len); } if (!content_undo_save_state()) return strlcpy(s, msg_hash_to_str(MSG_FAILED_TO_UNDO_SAVE_STATE), len); return strlcpy(s, msg_hash_to_str(MSG_UNDOING_SAVE_STATE), len); } static size_t command_event_undo_load_state(char *s, size_t len) { if (content_undo_load_buf_is_empty()) { enum msg_hash_enums msg = content_undo_save_disabled() ? MSG_CORE_DOES_NOT_SUPPORT_SAVESTATE_UNDO : MSG_NO_STATE_HAS_BEEN_LOADED_YET; return strlcpy(s, msg_hash_to_str(msg), len); } if (!content_undo_load_state()) return snprintf(s, len, "%s \"%s\".", msg_hash_to_str(MSG_FAILED_TO_UNDO_LOAD_STATE), "RAM"); #ifdef HAVE_NETWORKING netplay_driver_ctl(RARCH_NETPLAY_CTL_LOAD_SAVESTATE, NULL); #endif return strlcpy(s, msg_hash_to_str(MSG_UNDID_LOAD_STATE), len); } bool command_event_resize_windowed_scale(settings_t *settings, unsigned window_scale) { unsigned idx = 0; bool video_fullscreen = settings->bools.video_fullscreen; if (window_scale == 0) return false; configuration_set_uint(settings, settings->uints.video_scale, window_scale); if (!video_fullscreen) command_event(CMD_EVENT_REINIT, NULL); retroarch_ctl(RARCH_CTL_SET_WINDOWED_SCALE, &idx); return true; } size_t command_event_save_auto_state(void) { size_t _len; runloop_state_t *runloop_st = runloop_state_get_ptr(); const char *name_savestate = runloop_st->name.savestate; char savestate_name_auto[PATH_MAX_LENGTH]; const char *a = NULL; if (!core_info_current_supports_savestate()) return 0; a = path_basename(path_get(RARCH_PATH_BASENAME)); if (!a || !*a) return 0; _len = strlcpy(savestate_name_auto, name_savestate, sizeof(savestate_name_auto)); _len += strlcpy(savestate_name_auto + _len, ".auto", sizeof(savestate_name_auto) - _len); if (content_auto_save_state((const char*)savestate_name_auto)) RARCH_LOG("[State] %s \"%s\" %s.\n", msg_hash_to_str(MSG_AUTO_SAVE_STATE_TO), savestate_name_auto, "succeeded"); else RARCH_LOG("[State] %s \"%s\" %s.\n", msg_hash_to_str(MSG_AUTO_SAVE_STATE_TO), savestate_name_auto, "failed"); return _len; } #ifdef HAVE_CHEATS void command_event_init_cheats( bool apply_cheats_after_load, const char *path_cheat_db, void *bsv_movie_data) { #ifdef HAVE_NETWORKING bool allow_cheats = !netplay_driver_ctl( RARCH_NETPLAY_CTL_IS_DATA_INITED, NULL); #else bool allow_cheats = true; #endif #ifdef HAVE_BSV_MOVIE allow_cheats &= !(bsv_movie_data != NULL); #endif if (!allow_cheats) return; cheat_manager_alloc_if_empty(); cheat_manager_load_game_specific_cheats(path_cheat_db); if (apply_cheats_after_load) cheat_manager_apply_cheats( config_get_ptr()->bools.notification_show_cheats_applied); } #endif bool command_event_load_entry_state(settings_t *settings) { char entry_state_path[PATH_MAX_LENGTH] = ""; runloop_state_t *runloop_st = runloop_state_get_ptr(); bool ret = false; /* No early save-capability gate here: content_load_state() decides, * and an existing entry-state file outranks stale metadata. */ #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) return false; #endif #ifdef HAVE_NETWORKING if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL)) return false; #endif if (!runloop_get_entry_state_path( entry_state_path, sizeof(entry_state_path), runloop_st->entry_state_slot)) return false; if (!path_is_valid(entry_state_path)) { if (!runloop_get_savestate_path( entry_state_path, sizeof(entry_state_path), runloop_st->entry_state_slot)) return false; } if (!path_is_valid(entry_state_path)) return false; ret = content_load_state(entry_state_path, false, true); RARCH_LOG("[State] %s \"%s\".\n", msg_hash_to_str(MSG_FOUND_ENTRY_STATE_IN), entry_state_path); RARCH_LOG("[State] %s \"%s\" %s.\n", msg_hash_to_str(MSG_LOADING_ENTRY_STATE_FROM), entry_state_path, ret ? "succeeded" : "failed"); return ret; } bool command_event_load_auto_state(void) { size_t _len; char savestate_name_auto[PATH_MAX_LENGTH]; runloop_state_t *runloop_st = runloop_state_get_ptr(); const char *name_savestate = runloop_st->name.savestate; bool ret = false; /* No early save-capability gate here: content_load_state() decides, * and an existing .auto state file outranks stale metadata. */ #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) return false; #endif #ifdef HAVE_NETWORKING if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL)) return false; #endif _len = strlcpy(savestate_name_auto, name_savestate, sizeof(savestate_name_auto)); strlcpy(savestate_name_auto + _len, ".auto", sizeof(savestate_name_auto) - _len); if (!path_is_valid(savestate_name_auto)) return false; ret = content_load_state(savestate_name_auto, false, true); RARCH_LOG("[State] %s \"%s\".\n", msg_hash_to_str(MSG_FOUND_AUTO_SAVESTATE_IN), savestate_name_auto); RARCH_LOG("[State] %s \"%s\" %s.\n", msg_hash_to_str(MSG_AUTOLOADING_SAVESTATE_FROM), savestate_name_auto, ret ? "succeeded" : "failed"); return ret; } /** * Scans existing states to determine which one should be loaded * and which one can be deleted, using savestate wraparound if * enabled. * * @param settings The usual RetroArch settings ptr. * @param last_index Return value for load slot. * @param @s Return value for file name that should be removed. */ static void command_scan_states( bool show_hidden_files, unsigned savestate_max_keep, int curr_state_slot, unsigned *last_index, char *s) { /* Base name of 128 may be too short for some (<<1%) of the tosec-based file names, but in practice truncating will not lead to mismatch */ char state_base[128]; char state_dir[DIR_MAX_LENGTH]; runloop_state_t *runloop_st = runloop_state_get_ptr(); const char *name_savestate = runloop_st->name.savestate; unsigned max_idx = 0; unsigned loa_idx = 0; unsigned gap_idx = UINT_MAX; unsigned del_idx = UINT_MAX; retro_bits_512_t slot_mapping_low = {0}; retro_bits_512_t slot_mapping_high = {0}; struct string_list *dir_list = NULL; const char *savefile_root = NULL; size_t savefile_root_length = 0; size_t i, cnt = 0; size_t cnt_in_range = 0; fill_pathname_basedir(state_dir, name_savestate, sizeof(state_dir)); if (!(dir_list = dir_list_new_special(state_dir, DIR_LIST_PLAIN, NULL, show_hidden_files))) return; fill_pathname_base(state_base, name_savestate, sizeof(state_base)); for (i = 0; i < dir_list->size; i++) { unsigned idx; size_t _len; char elem_base[128]; const char *ext = NULL; const char *end = NULL; const char *dir_elem = dir_list->elems[i].data; if (!dir_elem || !*dir_elem) continue; _len = strlen(dir_elem); fill_pathname_base(elem_base, dir_elem, sizeof(elem_base)); /* Only consider files with a '.state' extension * > i.e. Ignore '.state.auto', '.state.bak', etc. */ ext = path_get_extension(elem_base); if ( (!ext || !*ext) || !string_starts_with_size(ext, "state", STRLEN_CONST("state"))) continue; /* Check whether this file is associated with * the current content */ if (!string_starts_with(elem_base, state_base)) continue; /* This looks like a valid savestate */ /* Save filename root and length (once) */ if (savefile_root_length == 0) { savefile_root = dir_elem; savefile_root_length = _len; } /* Decode the savestate index */ end = dir_elem + _len; while ((end > dir_elem) && ISDIGIT((int)end[-1])) { end--; if (savefile_root == dir_elem) savefile_root_length--; } idx = string_to_unsigned(end); /* Simple administration: max, total. */ if (idx > max_idx) max_idx = idx; cnt++; if (idx <= savestate_max_keep) cnt_in_range++; /* Maintain a 2x512 bit map of occupied save states */ if (idx < 512) BIT512_SET(slot_mapping_low,idx); else if (idx < 1024) BIT512_SET(slot_mapping_high, idx - 512); } /* Next loop on the bitmap, since the file system may have presented the files in any order above */ for (i = 0; i <= savestate_max_keep; i++) { /* Unoccupied save slots */ if ( (i < 512 && !BIT512_GET(slot_mapping_low, i)) || (i > 511 && !BIT512_GET(slot_mapping_high, i-512))) { /* Gap index: lowest free slot in the wraparound range */ if (gap_idx == UINT_MAX) gap_idx = (unsigned)i; } else /* Occupied save slots */ { /* Del index: first occupied slot in the wraparound range, after gap index */ if ( gap_idx < UINT_MAX && del_idx == UINT_MAX) del_idx = (unsigned)i; } } /* Special cases of wraparound */ /* No previous savestate - set to end, so that first save goes to 0 */ if (cnt_in_range == 0) { if (cnt == 0) loa_idx = savestate_max_keep; /* Transient: nothing in current range, but something is present * higher up -> load that */ else loa_idx = max_idx; gap_idx = savestate_max_keep; del_idx = savestate_max_keep; } /* No gap was found - deduct from current index or default and set (missing) gap index to be deleted */ else if (gap_idx == UINT_MAX) { /* Transient: no gap, and max is higher than currently * allowed -> load that, but wrap around so that next * time gap will be present */ if (max_idx > savestate_max_keep) { loa_idx = max_idx; gap_idx = 1; } /* Current index is in range, so let's assume it is correct */ else if ( (unsigned)curr_state_slot < savestate_max_keep) { loa_idx = curr_state_slot; gap_idx = curr_state_slot + 1; } else { loa_idx = savestate_max_keep; gap_idx = 0; } del_idx = gap_idx; } /* Gap was found */ else { /* No candidate to delete */ /* Either gap is at the end of the range: wraparound. or there is no better idea than the lowest index */ if (del_idx == UINT_MAX) del_idx = 0; /* Adjust load index */ if (gap_idx == 0) loa_idx = savestate_max_keep; else loa_idx = gap_idx - 1; } RARCH_DBG("[State] Save state scanning finished, used slots (in range): " "%d (%d), max:%d, load index %d, gap index %d, delete index %d.\n", cnt, cnt_in_range, max_idx, loa_idx, gap_idx, del_idx); if (last_index) *last_index = loa_idx; if ( s && cnt_in_range >= savestate_max_keep) { strlcpy(s, savefile_root, savefile_root_length + 1); /* ".state0" is just ".state" instead, so don't print that. */ if (del_idx > 0) snprintf(s + savefile_root_length, 5, "%d", del_idx); } dir_list_free(dir_list); } /** * Determines next savestate slot in case of auto-increment, * i.e. save state scanning was done already earlier. * Logic moved here so that all save state wraparound code is * in this file. * * @param settings The usual RetroArch settings ptr. * @return \c The next savestate slot. */ int command_event_get_next_savestate_auto_index(settings_t *settings) { unsigned savestate_max_keep = settings->uints.savestate_max_keep; int new_state_slot = settings->ints.state_slot + 1; /* If previous save was above the wraparound range, or it overflows, return to the start of the range. */ if ( (savestate_max_keep > 0) && (unsigned)new_state_slot > savestate_max_keep) return 0; return new_state_slot; } /** * Determines most recent savestate slot in case of content load. * * @param settings The usual RetroArch settings ptr. * @return \c The most recent savestate slot. */ void command_event_set_savestate_auto_index(settings_t *settings) { unsigned max_idx = 0; bool savestate_auto_index = settings->bools.savestate_auto_index; if (savestate_auto_index) { int prev_slot = settings->ints.state_slot; command_scan_states( settings->bools.show_hidden_files, settings->uints.savestate_max_keep, settings->ints.state_slot, &max_idx, NULL); configuration_set_int(settings, settings->ints.state_slot, max_idx); RARCH_LOG("[State] %s: #%d (slot reset %d -> %u from on-disk scan, " "max_keep %u). If the previous slot was higher, earlier saves " "may be missing on disk.\n", msg_hash_to_str(MSG_FOUND_LAST_STATE_SLOT), max_idx, prev_slot, max_idx, settings->uints.savestate_max_keep); } else /* Reset savestate index to 0 when loading content. */ configuration_set_int(settings, settings->ints.state_slot, 0); } /** * Deletes the oldest save state and its thumbnail, if needed. * * @param settings The usual RetroArch settings ptr. */ static void command_event_set_savestate_garbage_collect(settings_t *settings) { size_t i; char state_to_delete[PATH_MAX_LENGTH] = {0}; command_scan_states( settings->bools.show_hidden_files, settings->uints.savestate_max_keep, settings->ints.state_slot, NULL, state_to_delete); /* Only delete one save state per save action * > Conservative behaviour, designed to minimise * the risk of deleting multiple incorrect files * in case of accident */ if (*state_to_delete) { filestream_delete(state_to_delete); RARCH_DBG("[State] Garbage collect, deleting \"%s\".\n",state_to_delete); /* Construct the save state thumbnail name * and delete that one as well. */ i = strlen(state_to_delete); strlcpy(state_to_delete + i,".png",STRLEN_CONST(".png")+1); filestream_delete(state_to_delete); RARCH_DBG("[State] Garbage collect, deleting \"%s\".\n",state_to_delete); } } void command_event_set_replay_auto_index(settings_t *settings) { size_t i; char elem_base[128]; char state_base[128]; char state_dir[DIR_MAX_LENGTH]; struct string_list *dir_list = NULL; unsigned max_idx = 0; runloop_state_t *runloop_st = runloop_state_get_ptr(); const char *name_replay = runloop_st->name.replay; bool replay_auto_index = settings->bools.replay_auto_index; bool show_hidden_files = settings->bools.show_hidden_files; if (!replay_auto_index) return; /* Find the file in the same directory as runloop_st->name.replay * with the largest numeral suffix. * * E.g. /foo/path/content.replay will try to find * /foo/path/content.replay%d, where %d is the largest number available. */ fill_pathname_basedir(state_dir, name_replay, sizeof(state_dir)); if (!(dir_list = dir_list_new_special(state_dir, DIR_LIST_PLAIN, NULL, show_hidden_files))) return; fill_pathname_base(state_base, name_replay, sizeof(state_base)); for (i = 0; i < dir_list->size; i++) { unsigned idx; const char *end = NULL; const char *dir_elem = dir_list->elems[i].data; size_t _len = fill_pathname_base(elem_base, dir_elem, sizeof(elem_base)); if (strstr(elem_base, state_base) != elem_base) continue; end = dir_elem + _len; while ((end > dir_elem) && ISDIGIT((int)end[-1])) end--; idx = (unsigned)strtoul(end, NULL, 0); if (idx > max_idx) max_idx = idx; } dir_list_free(dir_list); configuration_set_int(settings, settings->ints.replay_slot, max_idx); if (max_idx) RARCH_LOG("[Replay] %s: #%d\n", msg_hash_to_str(MSG_FOUND_LAST_REPLAY_SLOT), max_idx); } void command_event_set_replay_garbage_collect( unsigned max_to_keep, bool show_hidden_files ) { /* TODO: debugme */ size_t i, cnt = 0; char tmp[DIR_MAX_LENGTH]; runloop_state_t *runloop_st = runloop_state_get_ptr(); const char *name_replay = runloop_st->name.replay; struct string_list *dir_list = NULL; unsigned min_idx = UINT_MAX; const char *oldest_save = NULL; /* Similar to command_event_set_replay_auto_index(), * this will find the lowest numbered replay */ fill_pathname_basedir(tmp, name_replay, sizeof(tmp)); if (!(dir_list = dir_list_new_special(tmp, DIR_LIST_PLAIN, NULL, show_hidden_files))) return; fill_pathname_base(tmp, name_replay, sizeof(tmp)); for (i = 0; i < dir_list->size; i++) { unsigned idx; size_t _len; char elem_base[128]; const char *ext = NULL; const char *end = NULL; const char *dir_elem = dir_list->elems[i].data; if (!dir_elem || !*dir_elem) continue; _len = fill_pathname_base(elem_base, dir_elem, sizeof(elem_base)); /* Only consider files with a '.replayXX' extension * > i.e. Ignore '.replay.auto', '.replay.bak', etc. */ ext = path_get_extension(elem_base); if ( (!ext || !*ext) || !string_starts_with_size(ext, "replay", STRLEN_CONST("REPLAY"))) continue; /* Check whether this file is associated with * the current content */ if (!string_starts_with(elem_base, tmp)) continue; /* This looks like a valid save */ cnt++; /* > Get index */ end = dir_elem + _len; while ((end > dir_elem) && ISDIGIT((int)end[-1])) end--; idx = string_to_unsigned(end); /* > Check if this is the lowest index so far */ if (idx < min_idx) { min_idx = idx; oldest_save = dir_elem; } } /* Only delete one save state per save action * > Conservative behaviour, designed to minimise * the risk of deleting multiple incorrect files * in case of accident */ if (oldest_save && *oldest_save && (cnt > max_to_keep)) filestream_delete(oldest_save); dir_list_free(dir_list); } #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) bool command_set_shader(command_t *cmd, const char *arg) { enum rarch_shader_type type = video_shader_parse_type(arg); settings_t *settings = config_get_ptr(); bool apply_new_shader = arg && *arg; configuration_set_bool(settings, settings->bools.video_shader_enable, apply_new_shader); if (apply_new_shader) { gfx_ctx_flags_t flags; flags.flags = 0; video_context_driver_get_flags(&flags); if (!BIT32_GET(flags.flags, video_shader_type_to_flag(type))) return false; /* rebase on shader directory */ if (!path_is_absolute(arg)) { char abs_arg[PATH_MAX_LENGTH]; const char *ref_path = settings->paths.directory_video_shader; fill_pathname_join_special(abs_arg, ref_path, arg, sizeof(abs_arg)); return video_shader_apply_shader(settings, type, abs_arg, true); } } return video_shader_apply_shader(settings, type, arg, true); } #endif #ifdef HAVE_CONFIGFILE bool command_event_save_core_config( const char *dir_menu_config, const char *rarch_path_config) { char msg[128]; char config_dir[DIR_MAX_LENGTH]; char config_path[PATH_MAX_LENGTH]; char config_name[NAME_MAX_LENGTH]; size_t _len = 0; bool new_path_available = false; bool overrides_active = false; const char *core_path = NULL; runloop_state_t *runloop_st = runloop_state_get_ptr(); msg[0] = '\0'; if (dir_menu_config && *dir_menu_config) _len = strlcpy(config_dir, dir_menu_config, sizeof(config_dir)); else if (rarch_path_config && *rarch_path_config) /* Fallback */ _len = fill_pathname_basedir(config_dir, rarch_path_config, sizeof(config_dir)); if (_len == 0) { const char *_msg = msg_hash_to_str(MSG_CONFIG_DIRECTORY_NOT_SET); runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); RARCH_ERR("[Config] %s\n", _msg); return false; } core_path = path_get(RARCH_PATH_CORE); /* Infer file name based on libretro core. */ if (path_is_valid(core_path)) { unsigned i; char tmp[PATH_MAX_LENGTH + 8]; RARCH_LOG("[Config] %s\n", msg_hash_to_str(MSG_USING_CORE_NAME_FOR_NEW_CONFIG)); fill_pathname(config_name, path_basename(core_path), "", sizeof(config_name)); fill_pathname_join_special(config_path, config_dir, config_name, sizeof(config_path)); /* In case of collision, find an alternative name. */ for (i = 0; i < 16; i++) { size_t __len = strlcpy(tmp, config_path, sizeof(tmp)); if (i) __len += snprintf(tmp + __len, sizeof(tmp) - __len, "-%u", i); strlcpy(tmp + __len, ".cfg", sizeof(tmp) - __len); if (!path_is_valid(tmp)) { strlcpy(config_path, tmp, sizeof(config_path)); new_path_available = true; break; } } } if (!new_path_available) { /* Fallback to system time... */ RARCH_WARN("[Config] %s\n", msg_hash_to_str(MSG_CANNOT_INFER_NEW_CONFIG_PATH)); fill_dated_filename(config_name, ".cfg", sizeof(config_name)); fill_pathname_join_special(config_path, config_dir, config_name, sizeof(config_path)); } if (runloop_st->flags & RUNLOOP_FLAG_OVERRIDES_ACTIVE) { /* Overrides block config file saving, * make it appear as overrides weren't enabled * for a manual save. */ runloop_st->flags &= ~RUNLOOP_FLAG_OVERRIDES_ACTIVE; overrides_active = true; } #ifdef HAVE_CONFIGFILE _len = command_event_save_config(config_path, msg, sizeof(msg)); #endif if (_len > 0) runloop_msg_queue_push(msg, _len, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); if (overrides_active) runloop_st->flags |= RUNLOOP_FLAG_OVERRIDES_ACTIVE; else runloop_st->flags &= ~RUNLOOP_FLAG_OVERRIDES_ACTIVE; return true; } void command_event_save_current_config(enum override_type type) { runloop_state_t *runloop_st = runloop_state_get_ptr(); switch (type) { default: case OVERRIDE_NONE: { size_t _len; char msg[256]; enum message_queue_category msg_cat = MESSAGE_QUEUE_CATEGORY_INFO; msg[0] = '\0'; if (path_is_empty(RARCH_PATH_CONFIG)) { msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR; _len = strlcpy(msg, "Config directory not set, cannot save configuration.", sizeof(msg)); } else { if (runloop_st->flags & RUNLOOP_FLAG_OVERRIDES_ACTIVE) { msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR; _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ACTIVE_NOT_SAVING), sizeof(msg)); } else { msg_cat = MESSAGE_QUEUE_CATEGORY_SUCCESS; _len = command_event_save_config(path_get(RARCH_PATH_CONFIG), msg, sizeof(msg)); } } /* command_event_save_config() does its own logging */ if (msg_cat == MESSAGE_QUEUE_CATEGORY_ERROR) RARCH_ERR("[Override] %s\n", msg); runloop_msg_queue_push(msg, _len, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, msg_cat); } break; case OVERRIDE_GAME: case OVERRIDE_CORE: case OVERRIDE_CONTENT_DIR: { size_t _len; char msg[256]; int8_t ret = config_save_overrides(type, &runloop_st->system, false, NULL); enum message_queue_category msg_cat = MESSAGE_QUEUE_CATEGORY_INFO; switch (ret) { case 1: msg_cat = MESSAGE_QUEUE_CATEGORY_SUCCESS; _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_SAVED_SUCCESSFULLY), sizeof(msg)); /* set overrides to active so the original config can be restored after closing content */ runloop_st->flags |= RUNLOOP_FLAG_OVERRIDES_ACTIVE; break; case -1: msg_cat = MESSAGE_QUEUE_CATEGORY_WARNING; _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_NOT_SAVED), sizeof(msg)); break; default: case 0: msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR; _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ERROR_SAVING), sizeof(msg)); break; } switch (msg_cat) { case MESSAGE_QUEUE_CATEGORY_ERROR: RARCH_ERR("[Override] %s\n", msg); break; case MESSAGE_QUEUE_CATEGORY_WARNING: RARCH_WARN("[Override] %s\n", msg); break; case MESSAGE_QUEUE_CATEGORY_SUCCESS: default: RARCH_LOG("[Override] %s\n", msg); break; } runloop_msg_queue_push(msg, _len, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, msg_cat); #ifdef HAVE_MENU { struct menu_state *menu_st = menu_state_get_ptr(); menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH | MENU_ST_FLAG_PREVENT_POPULATE; } #endif } break; } } void command_event_remove_current_config(enum override_type type) { runloop_state_t *runloop_st = runloop_state_get_ptr(); switch (type) { default: case OVERRIDE_NONE: break; case OVERRIDE_GAME: case OVERRIDE_CORE: case OVERRIDE_CONTENT_DIR: { size_t _len; char msg[256]; enum message_queue_category msg_cat = MESSAGE_QUEUE_CATEGORY_INFO; if (config_save_overrides(type, &runloop_st->system, true, NULL)) { msg_cat = MESSAGE_QUEUE_CATEGORY_SUCCESS; _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_REMOVED_SUCCESSFULLY), sizeof(msg)); } else { msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR; _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ERROR_REMOVING), sizeof(msg)); } if (msg_cat == MESSAGE_QUEUE_CATEGORY_ERROR) RARCH_ERR("[Override] %s\n", msg); else RARCH_LOG("[Override] %s\n", msg); runloop_msg_queue_push(msg, _len, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, msg_cat); #ifdef HAVE_MENU { struct menu_state *menu_st = menu_state_get_ptr(); menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH | MENU_ST_FLAG_PREVENT_POPULATE; } #endif } break; } } #endif bool command_event_main_state(unsigned cmd) { char msg[128] = ""; char state_path[PATH_MAX_LENGTH] = ""; size_t _len = 0; settings_t *settings = config_get_ptr(); bool savestates_enabled = core_info_current_supports_savestate(); bool ret = false; if (savestates_enabled) { size_t info_size; runloop_get_current_savestate_path(state_path, sizeof(state_path)); info_size = core_serialize_size(); savestates_enabled = (info_size > 0); } /* TODO: Load state should act in one of three ways: - [X] Not during recording or playback: normally - [-] During playback: If the state is part of this replay, go back to that state and rewind the replay (not yet implemented); otherwise halt playback and go to that state normally. - [-] During recording: If the state is part of this replay, go back to that state and rewind the replay, clobbering the stuff in between then and now (not yet implemented); if the state is not part of the replay, do nothing and log a warning. */ if (savestates_enabled) { switch (cmd) { case CMD_EVENT_SAVE_STATE: case CMD_EVENT_SAVE_STATE_TO_RAM: { /* TODO: Saving state during recording should associate * the state with the replay. */ video_driver_state_t *video_st = video_state_get_ptr(); bool savestate_auto_index = settings->bools.savestate_auto_index; unsigned savestate_max_keep = settings->uints.savestate_max_keep; bool frame_time_counter_auto_reset = settings->bools.frame_time_counter_auto_reset; if (cmd == CMD_EVENT_SAVE_STATE) { bool queued = content_save_state(state_path, true); RARCH_LOG("[State] save dispatch for slot %d, path " "\"%s\": content_save_state queued=%s " "(auto_index=%s, max_keep=%u). NOTE: actual disk " "write is asynchronous; success is only known when " "the save task completes.\n", settings->ints.state_slot, state_path, queued ? "yes" : "NO", savestate_auto_index ? "on" : "off", savestate_max_keep); } else content_save_state_to_ram(); /* Clean up excess savestates if necessary */ if (savestate_auto_index && (savestate_max_keep > 0)) command_event_set_savestate_garbage_collect(settings); if (frame_time_counter_auto_reset) video_st->frame_time_count = 0; ret = true; } break; case CMD_EVENT_LOAD_STATE: case CMD_EVENT_LOAD_STATE_FROM_RAM: { bool res = false; if (cmd == CMD_EVENT_LOAD_STATE) res = content_load_state(state_path, false, false); else res = content_load_state_from_ram(); if (res) { command_post_state_loaded(); ret = true; } } break; case CMD_EVENT_UNDO_LOAD_STATE: { /* TODO: To support this through re-recording would take some * care around moving the replay recording forward to the time * when the undo happened, which would need undo support for * replays. For now, forbid it during recording and halt * playback. */ #ifdef HAVE_BSV_MOVIE input_driver_state_t *input_st = input_state_get_ptr(); if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_RECORDING) { RARCH_ERR("[State] Can't undo load state during movie record.\n"); return false; } if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PLAYBACK) { RARCH_LOG("[State] Undo load state during movie playback, halting playback.\n"); movie_stop(input_st); } #endif _len = command_event_undo_load_state(msg, sizeof(msg)); ret = true; break; } case CMD_EVENT_UNDO_SAVE_STATE: _len = command_event_undo_save_state(msg, sizeof(msg)); ret = true; break; } } else _len = strlcpy(msg, msg_hash_to_str( MSG_CORE_DOES_NOT_SUPPORT_SAVESTATES), sizeof(msg)); if (_len > 0) { runloop_msg_queue_push(msg, _len, 2, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_LOG("[State] %s\n", msg); } return ret; } bool command_event_disk_control_append_image( const char *path) { runloop_state_t *runloop_st = runloop_state_get_ptr(); rarch_system_info_t *sys_info = runloop_st ? (rarch_system_info_t*)&runloop_st->system : NULL; if ( !sys_info || !disk_control_append_image(&sys_info->disk_control, path)) return false; #ifdef HAVE_THREADS if (runloop_st->flags & RUNLOOP_FLAG_USE_SRAM) autosave_deinit(); #endif command_event(CMD_EVENT_AUTOSAVE_INIT, NULL); return true; } /* Read-side callback for the snapshot phase of command_event_reinit. * Receives the cached frame's pixels (or NULL if HW-render / no * cached frame), copies into the static reuse buffer, and reports * dims back to the caller via userdata. Skips the work entirely * if data is NULL -- caller checks the reported size to know * whether a snapshot was actually taken. */ struct command_reinit_snapshot_ctx { void **buf_p; /* static cached_snapshot in the caller */ size_t *cap_p; /* static cached_snapshot_cap in the caller */ unsigned w, h; size_t p, size; }; static void command_reinit_snapshot_cb(void *userdata, const void *data, unsigned width, unsigned height, size_t pitch) { struct command_reinit_snapshot_ctx *ctx = (struct command_reinit_snapshot_ctx*)userdata; size_t want; if (!ctx || !data || !width || !height || !pitch) return; want = pitch * height; if (want > *ctx->cap_p) { void *tmp = realloc(*ctx->buf_p, want); if (!tmp) return; *ctx->buf_p = tmp; *ctx->cap_p = want; } if (!*ctx->buf_p) return; memcpy(*ctx->buf_p, data, want); ctx->w = width; ctx->h = height; ctx->p = pitch; ctx->size = want; } void command_event_reinit(const int flags) { settings_t *settings = config_get_ptr(); input_driver_state_t *input_st = input_state_get_ptr(); video_driver_state_t *video_st = video_state_get_ptr(); #ifdef HAVE_MENU gfx_display_t *p_disp = disp_get_ptr(); struct menu_state *menu_st = menu_state_get_ptr(); bool video_fullscreen = settings->bools.video_fullscreen; bool adaptive_vsync = settings->bools.video_adaptive_vsync; unsigned swap_interval_config = settings->uints.video_swap_interval; #endif enum input_game_focus_cmd_type game_focus_cmd = GAME_FOCUS_CMD_REAPPLY; const input_device_driver_t *joypad = input_st->primary_joypad; #ifdef HAVE_MFI const input_device_driver_t *sec_joypad = input_st->secondary_joypad; #else const input_device_driver_t *sec_joypad = NULL; #endif /* Snapshot the last cached core frame before tearing the video * driver down. video_driver_free() invalidates the cache as * part of the reinit cycle (the pointer was borrowed from the * core's own framebuffer and isn't guaranteed to stay live * across the driver swap), so without a snapshot the new * driver would come up with no core image to replay. Restored * + replayed below so the paused-core background remains * visible when reinit is triggered from inside the menu (e.g. * HDR mode toggle). * * The snapshot buffer is static and reused across reinits — we * need it to outlive command_event_reinit because we publish * the pointer back through video_driver_cached_frame_publish * for the new driver to read via video_driver_cached_frame(), * and the core's next real frame will replace the pointer at * its leisure. Resizing in place on each call keeps it * bounded at one buffer's worth. * * The actual copy happens inside the cached_frame_read * callback (which is what this function was hand-rolling * before the unified read API existed -- exactly the same * pattern, lifted into one place). HW-render frames are * skipped: the callback receives data == NULL for those, so * we don't even allocate. */ static void *cached_snapshot = NULL; static size_t cached_snapshot_cap = 0; unsigned cached_snapshot_w = 0; unsigned cached_snapshot_h = 0; size_t cached_snapshot_p = 0; size_t cached_snapshot_size = 0; { struct command_reinit_snapshot_ctx ctx; ctx.buf_p = &cached_snapshot; ctx.cap_p = &cached_snapshot_cap; ctx.w = 0; ctx.h = 0; ctx.p = 0; ctx.size = 0; video_driver_cached_frame_read(&ctx, command_reinit_snapshot_cb); cached_snapshot_w = ctx.w; cached_snapshot_h = ctx.h; cached_snapshot_p = ctx.p; cached_snapshot_size = ctx.size; } (void)cached_snapshot_size; video_driver_reinit(flags); /* Restore the snapshot and ask the new driver to replay it so the * paused-core background appears in the first post-reinit frame. * The buffer stays live across subsequent frame_cb calls from the * core (which overwrite the pointer) and is reused on the next * reinit. The static cached_snapshot itself is not freed at * shutdown - it's a one-shot leak bounded at one framebuffer's * worth of memory, reclaimed by the OS on process exit. Adding * a teardown hook would mean wiring command_event_reinit's * statics into retroarch_deinit_drivers; the size cap makes the * leak benign in practice, so we leave it. */ if (cached_snapshot_p && cached_snapshot_h) { video_driver_cached_frame_publish(cached_snapshot, cached_snapshot_w, cached_snapshot_h, cached_snapshot_p); #ifdef HAVE_MENU /* If the menu is alive across the reinit, the runloop's * pre-frame menu work (driver_ctx->render + * set_texture_enable) doesn't get a chance to run before * the cached_frame() replay below. Two consequences if we * don't reproduce that work here: * * 1. The new video driver instance starts with its * menu-texture flag (D3D11_ST_FLAG_MENU_ENABLE, * GL2_FLAG_MENU_TEXTURE_ENABLE, etc.) cleared, so the * replay frame falls into the driver's "else if * (statistics_show)" branch and draws OSD stats on the * bare core framebuffer instead of the menu overlay. * * 2. Menu drivers like ozone cache layout/font dimensions * keyed on the previous viewport size and only * recompute them when their render() callback notices a * width/height change. If we replay before render() * runs, the menu draws at the old (windowed) scale into * the new (fullscreen) viewport, so fonts and widgets * appear undersized for one frame until the next * runloop iteration corrects them. * * Mirroring the runloop's pre-frame menu sequence here -- * render with the new dimensions, then raise the * texture-enable flag -- keeps the snapshot replay * visually consistent with subsequent frames. * * Gated on a valid snapshot: without one we won't be * pushing a replay frame anyway, and calling render() on a * freshly-(re)initialised menu driver before the runloop * has had a chance to populate it can leave it in a * partially-computed state for the next real frame * (e.g. ozone clears OZONE_FLAG_NEED_COMPUTE after a * premature render). */ if (menu_st->flags & MENU_ST_FLAG_ALIVE) { if ( menu_st->driver_ctx && menu_st->driver_ctx->render) menu_st->driver_ctx->render( menu_st->userdata, video_st->width, video_st->height, false); if ( video_st->poke && video_st->poke->set_texture_enable) video_st->poke->set_texture_enable(video_st->data, true, false); } #endif video_driver_cached_frame(); } /* Poll input to avoid possibly stale data to corrupt things. */ if (joypad && joypad->poll) joypad->poll(); if (sec_joypad && sec_joypad->poll) sec_joypad->poll(); if (input_st->current_driver && input_st->current_driver->poll) input_st->current_driver->poll(input_st->current_data); command_event(CMD_EVENT_GAME_FOCUS_TOGGLE, &game_focus_cmd); #ifdef HAVE_MENU p_disp->flags |= GFX_DISP_FLAG_FB_DIRTY; if (video_fullscreen) { if ( video_st->poke && video_st->poke->show_mouse) video_st->poke->show_mouse(video_st->data, false); } if ( (menu_st->flags & MENU_ST_FLAG_ALIVE) && video_st->current_video->set_nonblock_state) video_st->current_video->set_nonblock_state( video_st->data, false, video_driver_test_all_flags(GFX_CTX_FLAGS_ADAPTIVE_VSYNC) && adaptive_vsync, runloop_get_video_swap_interval(swap_interval_config)); #endif }