/* RetroArch - A frontend for libretro. * Copyright (C) 2010-2014 - Hans-Kristian Arntzen * Copyright (C) 2011-2017 - Daniel De Matteis * * 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 #include #include #include #include #include #include #include "configuration.h" #include "movie.h" #include "core.h" #include "content.h" #include "retroarch.h" #include "msg_hash.h" #include "verbosity.h" #include "command.h" #include "file_path_special.h" struct bsv_movie { intfstream_t *file; /* A ring buffer keeping track of positions * in the file for each frame. */ size_t *frame_pos; size_t frame_mask; size_t frame_ptr; size_t min_file_pos; size_t state_size; uint8_t *state; bool playback; bool first_rewind; bool did_rewind; }; struct bsv_state { bool movie_start_recording; bool movie_start_playback; bool movie_playback; bool eof_exit; bool movie_end; /* Movie playback/recording support. */ char movie_path[PATH_MAX_LENGTH]; /* Immediate playback/recording. */ char movie_start_path[PATH_MAX_LENGTH]; }; static bsv_movie_t *bsv_movie_state_handle = NULL; static struct bsv_state bsv_movie_state; static bool bsv_movie_init_playback(bsv_movie_t *handle, const char *path) { uint32_t state_size = 0; uint32_t content_crc = 0; uint32_t header[4] = {0}; intfstream_t *file = intfstream_open_file(path, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); if (!file) { RARCH_ERR("Could not open BSV file for playback, path : \"%s\".\n", path); return false; } handle->file = file; handle->playback = true; intfstream_read(handle->file, header, sizeof(uint32_t) * 4); /* Compatibility with old implementation that * used incorrect documentation. */ if (swap_if_little32(header[MAGIC_INDEX]) != BSV_MAGIC && swap_if_big32(header[MAGIC_INDEX]) != BSV_MAGIC) { RARCH_ERR("%s\n", msg_hash_to_str(MSG_MOVIE_FILE_IS_NOT_A_VALID_BSV1_FILE)); return false; } content_crc = content_get_crc(); if (content_crc != 0) if (swap_if_big32(header[CRC_INDEX]) != content_crc) RARCH_WARN("%s.\n", msg_hash_to_str(MSG_CRC32_CHECKSUM_MISMATCH)); state_size = swap_if_big32(header[STATE_SIZE_INDEX]); #if 0 RARCH_ERR("----- debug %u -----\n", header[0]); RARCH_ERR("----- debug %u -----\n", header[1]); RARCH_ERR("----- debug %u -----\n", header[2]); RARCH_ERR("----- debug %u -----\n", header[3]); #endif if (state_size) { retro_ctx_size_info_t info; retro_ctx_serialize_info_t serial_info; uint8_t *buf = (uint8_t*)malloc(state_size); if (!buf) return false; handle->state = buf; handle->state_size = state_size; if (intfstream_read(handle->file, handle->state, state_size) != state_size) { RARCH_ERR("%s\n", msg_hash_to_str(MSG_COULD_NOT_READ_STATE_FROM_MOVIE)); return false; } core_serialize_size( &info); if (info.size == state_size) { serial_info.data_const = handle->state; serial_info.size = state_size; core_unserialize(&serial_info); } else RARCH_WARN("%s\n", msg_hash_to_str(MSG_MOVIE_FORMAT_DIFFERENT_SERIALIZER_VERSION)); } handle->min_file_pos = sizeof(header) + state_size; return true; } static bool bsv_movie_init_record(bsv_movie_t *handle, const char *path) { retro_ctx_size_info_t info; uint32_t state_size = 0; uint32_t content_crc = 0; uint32_t header[4] = {0}; intfstream_t *file = intfstream_open_file(path, RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); if (!file) { RARCH_ERR("Could not open BSV file for recording, path : \"%s\".\n", path); return false; } handle->file = file; content_crc = content_get_crc(); /* This value is supposed to show up as * BSV1 in a HEX editor, big-endian. */ header[MAGIC_INDEX] = swap_if_little32(BSV_MAGIC); header[CRC_INDEX] = swap_if_big32(content_crc); core_serialize_size(&info); state_size = (unsigned)info.size; header[STATE_SIZE_INDEX] = swap_if_big32(state_size); #if 0 RARCH_ERR("----- debug %u -----\n", header[0]); RARCH_ERR("----- debug %u -----\n", header[1]); RARCH_ERR("----- debug %u -----\n", header[2]); RARCH_ERR("----- debug %u -----\n", header[3]); #endif intfstream_write(handle->file, header, 4 * sizeof(uint32_t)); handle->min_file_pos = sizeof(header) + state_size; handle->state_size = state_size; if (state_size) { retro_ctx_serialize_info_t serial_info; handle->state = (uint8_t*)malloc(state_size); if (!handle->state) return false; serial_info.data = handle->state; serial_info.size = state_size; core_serialize(&serial_info); intfstream_write(handle->file, handle->state, state_size); } return true; } static void bsv_movie_free(bsv_movie_t *handle) { if (!handle) return; intfstream_close(handle->file); free(handle->file); free(handle->state); free(handle->frame_pos); free(handle); } static bsv_movie_t *bsv_movie_init_internal(const char *path, enum rarch_movie_type type) { size_t *frame_pos = NULL; bsv_movie_t *handle = (bsv_movie_t*)calloc(1, sizeof(*handle)); if (!handle) return NULL; if (type == RARCH_MOVIE_PLAYBACK) { if (!bsv_movie_init_playback(handle, path)) goto error; } else if (!bsv_movie_init_record(handle, path)) goto error; /* Just pick something really large * ~1 million frames rewind should do the trick. */ if (!(frame_pos = (size_t*)calloc((1 << 20), sizeof(size_t)))) goto error; handle->frame_pos = frame_pos; handle->frame_pos[0] = handle->min_file_pos; handle->frame_mask = (1 << 20) - 1; return handle; error: bsv_movie_free(handle); return NULL; } /* Used for rewinding while playback/record. */ void bsv_movie_set_frame_start(void) { if (bsv_movie_state_handle) bsv_movie_state_handle->frame_pos[bsv_movie_state_handle->frame_ptr] = intfstream_tell(bsv_movie_state_handle->file); } void bsv_movie_set_frame_end(void) { if (!bsv_movie_state_handle) return; bsv_movie_state_handle->frame_ptr = (bsv_movie_state_handle->frame_ptr + 1) & bsv_movie_state_handle->frame_mask; bsv_movie_state_handle->first_rewind = !bsv_movie_state_handle->did_rewind; bsv_movie_state_handle->did_rewind = false; } static void bsv_movie_frame_rewind(bsv_movie_t *handle) { handle->did_rewind = true; if ( (handle->frame_ptr <= 1) && (handle->frame_pos[0] == handle->min_file_pos)) { /* If we're at the beginning... */ handle->frame_ptr = 0; intfstream_seek(handle->file, (int)handle->min_file_pos, SEEK_SET); } else { /* First time rewind is performed, the old frame is simply replayed. * However, playing back that frame caused us to read data, and push * data to the ring buffer. * * Sucessively rewinding frames, we need to rewind past the read data, * plus another. */ handle->frame_ptr = (handle->frame_ptr - (handle->first_rewind ? 1 : 2)) & handle->frame_mask; intfstream_seek(handle->file, (int)handle->frame_pos[handle->frame_ptr], SEEK_SET); } if (intfstream_tell(handle->file) <= (long)handle->min_file_pos) { /* We rewound past the beginning. */ if (!handle->playback) { retro_ctx_serialize_info_t serial_info; /* If recording, we simply reset * the starting point. Nice and easy. */ intfstream_seek(handle->file, 4 * sizeof(uint32_t), SEEK_SET); serial_info.data = handle->state; serial_info.size = handle->state_size; core_serialize(&serial_info); intfstream_write(handle->file, handle->state, handle->state_size); } else intfstream_seek(handle->file, (int)handle->min_file_pos, SEEK_SET); } } bool bsv_movie_init(void) { bool set_granularity = false; bool ret = true; if (bsv_movie_state.movie_start_playback) { ret = bsv_movie_init_handle(bsv_movie_state.movie_start_path, RARCH_MOVIE_PLAYBACK); if (!ret) { RARCH_ERR("%s: \"%s\".\n", msg_hash_to_str(MSG_FAILED_TO_LOAD_MOVIE_FILE), bsv_movie_state.movie_start_path); retroarch_fail(1, "event_init_movie()"); } bsv_movie_state.movie_playback = true; runloop_msg_queue_push(msg_hash_to_str(MSG_STARTING_MOVIE_PLAYBACK), 2, 180, false); RARCH_LOG("%s.\n", msg_hash_to_str(MSG_STARTING_MOVIE_PLAYBACK)); set_granularity = true; } else if (bsv_movie_state.movie_start_recording) { char msg[256]; snprintf(msg, sizeof(msg), "%s \"%s\".", msg_hash_to_str(MSG_STARTING_MOVIE_RECORD_TO), bsv_movie_state.movie_start_path); ret = bsv_movie_init_handle(bsv_movie_state.movie_start_path, RARCH_MOVIE_RECORD); if (!ret) { runloop_msg_queue_push( msg_hash_to_str(MSG_FAILED_TO_START_MOVIE_RECORD), 1, 180, true); RARCH_ERR("%s.\n", msg_hash_to_str(MSG_FAILED_TO_START_MOVIE_RECORD)); return ret; } runloop_msg_queue_push(msg, 1, 180, true); RARCH_LOG("%s \"%s\".\n", msg_hash_to_str(MSG_STARTING_MOVIE_RECORD_TO), bsv_movie_state.movie_start_path); set_granularity = true; } if (set_granularity) { settings_t *settings = config_get_ptr(); configuration_set_uint(settings, settings->uints.rewind_granularity, 1); } return ret; } bool bsv_movie_get_input(int16_t *bsv_data) { if (intfstream_read(bsv_movie_state_handle->file, bsv_data, 1) != 1) return false; *bsv_data = swap_if_big16(*bsv_data); return true; } bool bsv_movie_is_playback_on(void) { return bsv_movie_state_handle && bsv_movie_state.movie_playback; } bool bsv_movie_is_playback_off(void) { return bsv_movie_state_handle && !bsv_movie_state.movie_playback; } bool bsv_movie_is_end_of_file(void) { return bsv_movie_state.movie_end && bsv_movie_state.eof_exit; } bool bsv_movie_ctl(enum bsv_ctl_state state, void *data) { switch (state) { case BSV_MOVIE_CTL_IS_INITED: return (bsv_movie_state_handle != NULL); case BSV_MOVIE_CTL_SET_START_RECORDING: bsv_movie_state.movie_start_recording = true; break; case BSV_MOVIE_CTL_UNSET_START_RECORDING: bsv_movie_state.movie_start_recording = false; break; case BSV_MOVIE_CTL_SET_START_PLAYBACK: bsv_movie_state.movie_start_playback = true; break; case BSV_MOVIE_CTL_UNSET_START_PLAYBACK: bsv_movie_state.movie_start_playback = false; break; case BSV_MOVIE_CTL_SET_END_EOF: bsv_movie_state.eof_exit = true; break; case BSV_MOVIE_CTL_SET_END: bsv_movie_state.movie_end = true; break; case BSV_MOVIE_CTL_UNSET_END: bsv_movie_state.movie_end = false; break; case BSV_MOVIE_CTL_UNSET_PLAYBACK: bsv_movie_state.movie_playback = false; break; case BSV_MOVIE_CTL_FRAME_REWIND: bsv_movie_frame_rewind(bsv_movie_state_handle); break; case BSV_MOVIE_CTL_SET_INPUT: { int16_t *bsv_data = (int16_t*)data; *bsv_data = swap_if_big16(*bsv_data); intfstream_write(bsv_movie_state_handle->file, bsv_data, 1); } break; case BSV_MOVIE_CTL_NONE: default: return false; } return true; } void bsv_movie_set_path(const char *path) { strlcpy(bsv_movie_state.movie_path, path, sizeof(bsv_movie_state.movie_path)); } void bsv_movie_set_start_path(const char *path) { strlcpy(bsv_movie_state.movie_start_path, path, sizeof(bsv_movie_state.movie_start_path)); } bool bsv_movie_init_handle(const char *path, enum rarch_movie_type type) { bsv_movie_t *state = bsv_movie_init_internal(path, type); if (!state) return false; bsv_movie_state_handle = state; return true; } void bsv_movie_deinit(void) { if (!bsv_movie_state_handle) return; bsv_movie_free(bsv_movie_state_handle); bsv_movie_state_handle = NULL; } /* Checks if movie is being played back. */ static bool bsv_movie_check_movie_playback(void) { runloop_msg_queue_push( msg_hash_to_str(MSG_MOVIE_PLAYBACK_ENDED), 2, 180, false); RARCH_LOG("%s\n", msg_hash_to_str(MSG_MOVIE_PLAYBACK_ENDED)); command_event(CMD_EVENT_BSV_MOVIE_DEINIT, NULL); bsv_movie_state.movie_end = false; bsv_movie_state.movie_playback = false; return true; } /* Checks if movie is being recorded. */ static bool runloop_check_movie_record(void) { if (!bsv_movie_state_handle) return false; runloop_msg_queue_push( msg_hash_to_str(MSG_MOVIE_RECORD_STOPPED), 2, 180, true); RARCH_LOG("%s\n", msg_hash_to_str(MSG_MOVIE_RECORD_STOPPED)); command_event(CMD_EVENT_BSV_MOVIE_DEINIT, NULL); return true; } static bool runloop_check_movie_init(void) { char msg[128], path[PATH_MAX_LENGTH]; settings_t *settings = config_get_ptr(); msg[0] = path[0] = '\0'; configuration_set_uint(settings, settings->uints.rewind_granularity, 1); if (settings->ints.state_slot > 0) snprintf(path, sizeof(path), "%s%d", bsv_movie_state.movie_path, settings->ints.state_slot); else strlcpy(path, bsv_movie_state.movie_path, sizeof(path)); strlcat(path, file_path_str(FILE_PATH_BSV_EXTENSION), sizeof(path)); snprintf(msg, sizeof(msg), "%s \"%s\".", msg_hash_to_str(MSG_STARTING_MOVIE_RECORD_TO), path); bsv_movie_init_handle(path, RARCH_MOVIE_RECORD); if (!bsv_movie_state_handle) { runloop_msg_queue_push( msg_hash_to_str(MSG_FAILED_TO_START_MOVIE_RECORD), 2, 180, true); RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_START_MOVIE_RECORD)); return false; } runloop_msg_queue_push(msg, 2, 180, true); RARCH_LOG("%s \"%s\".\n", msg_hash_to_str(MSG_STARTING_MOVIE_RECORD_TO), path); return true; } bool bsv_movie_check(void) { if (!bsv_movie_state_handle) return runloop_check_movie_init(); if (bsv_movie_state.movie_playback) { if (!bsv_movie_state.movie_end) return false; return bsv_movie_check_movie_playback(); } return runloop_check_movie_record(); }