diff --git a/core/memory/savestates.cpp b/core/memory/savestates.cpp index 1e70d170..ccfc1e38 100644 --- a/core/memory/savestates.cpp +++ b/core/memory/savestates.cpp @@ -58,760 +58,809 @@ bool g_st_skip_dma = false; namespace Savestates { - /// Represents a task to be performed by the savestate system. - struct t_savestate_task - { - struct t_params - { - /// The savestate slot. - /// Valid if the task's medium is . - size_t slot; - - /// The path to the savestate file. - /// Valid if the task's medium is . - std::filesystem::path path; - - /// The buffer containing the savestate data. - /// Valid if the task's medium is and the job is . - std::vector buffer; - }; - - /// The job to perform. - Job job; - - /// The savestate's source or target medium. - Medium medium; - - /// Callback to invoke when the task finishes. Mustn't be null. - t_savestate_callback callback; - - /// The task's parameters. Only one field in the struct is valid at a time. - t_params params{}; - - /// Whether warnings, such as those about ROM compatibility, shouldn't be shown. - bool ignore_warnings; - }; - - // Enable fixing .st to work for old mupen and m64p - constexpr bool FIX_NEW_ST = true; - - // Bit to set in RDRAM register to indicate that the savestate has been fixed - constexpr auto RDRAM_DEVICE_MANUF_NEW_FIX_BIT = (1 << 31); - - // The task vector mutex. Locked when accessing the task vector. - std::recursive_mutex g_task_mutex; - - // The task vector, which contains the task queue to be performed by the savestate system. - std::vector g_tasks; - - // Demarcator for new screenshot section - char screen_section[] = "SCR"; - - // Buffer used for storing flashram data during loading - char g_flashram_buf[1024]{}; - - // Buffer used for storing event queue data during loading - char g_event_queue_buf[1024]{}; - - // Buffer used for storing st data up to event queue - uint8_t g_first_block[0xA02BB4 - 32]{}; - - void get_paths_for_task(const t_savestate_task& task, std::filesystem::path& st_path, std::filesystem::path& sd_path) - { - sd_path = std::format("{}{}.sd", get_saves_directory().string(), (const char*)ROM_HEADER.nom); - - if (task.medium == Medium::Slot) - { - st_path = std::format( - L"{}{} {}.st{}", - get_saves_directory().wstring(), - string_to_wstring((const char*)ROM_HEADER.nom), - country_code_to_country_name(ROM_HEADER.Country_code), std::to_wstring(task.params.slot)); - } - } - - void init() - { - } - - void load_memory_from_buffer(uint8_t* p) - { - memread(&p, &rdram_register, sizeof(RDRAM_register)); - if (rdram_register.rdram_device_manuf & RDRAM_DEVICE_MANUF_NEW_FIX_BIT) - { - rdram_register.rdram_device_manuf &= ~RDRAM_DEVICE_MANUF_NEW_FIX_BIT; //remove the trick - g_st_skip_dma = true; //tell dma.c to skip it - } - memread(&p, &MI_register, sizeof(mips_register)); - memread(&p, &pi_register, sizeof(PI_register)); - memread(&p, &sp_register, sizeof(SP_register)); - memread(&p, &rsp_register, sizeof(RSP_register)); - memread(&p, &si_register, sizeof(SI_register)); - memread(&p, &vi_register, sizeof(VI_register)); - memread(&p, &ri_register, sizeof(RI_register)); - memread(&p, &ai_register, sizeof(AI_register)); - memread(&p, &dpc_register, sizeof(DPC_register)); - memread(&p, &dps_register, sizeof(DPS_register)); - memread(&p, rdram, 0x800000); - memread(&p, SP_DMEM, 0x1000); - memread(&p, SP_IMEM, 0x1000); - memread(&p, PIF_RAM, 0x40); - - char buf[4 * 32]; - memread(&p, buf, 24); - load_flashram_infos(buf); - - memread(&p, tlb_LUT_r, 0x100000); - memread(&p, tlb_LUT_w, 0x100000); - - memread(&p, &llbit, 4); - memread(&p, reg, 32 * 8); - for (int32_t i = 0; i < 32; i++) - { - memread(&p, reg_cop0 + i, 4); - memread(&p, buf, 4); // for compatibility with old versions purpose - } - memread(&p, &lo, 8); - memread(&p, &hi, 8); - memread(&p, reg_cop1_fgr_64, 32 * 8); - memread(&p, &FCR0, 4); - memread(&p, &FCR31, 4); - memread(&p, tlb_e, 32 * sizeof(tlb)); - if (!dynacore && interpcore) - memread(&p, &interp_addr, 4); - else - { - uint32_t target_addr; - memread(&p, &target_addr, 4); - for (char& i : invalid_code) - i = 1; - jump_to(target_addr) - } - - memread(&p, &next_interrupt, 4); - memread(&p, &next_vi, 4); - memread(&p, &vi_field, 4); - } - - std::vector generate_savestate() - { - std::vector b; - - b.reserve(0xB624F0); - - memset(g_flashram_buf, 0, sizeof(g_flashram_buf)); - memset(g_event_queue_buf, 0, sizeof(g_event_queue_buf)); - - uint32_t movie_active = VCR::get_task() != e_task::idle; - - if (FIX_NEW_ST) - { - //this is code taken from dma.c:dma_si_read(), it finishes up the dma. - //it copies data from pif (should contain commands and controller states), updates count reg and adds SI interrupt to queue - //so why does old mupen and mupen64plus dislike savestates without doing this? well in case of mario 64 it leaves pif command buffer uninitialised - //and it never can poll input properly (hence the inability to frame advance which is handled inside controller read). - - //But we dont want to do this then load such .st and dma again... so I notify mupen about this in .st, - //since .st is filled up to the brim with data (not even a single unused offset) I have to store one bit in... rdram manufacturer register - //this 99.999% wont break on any game, and that bit will be cleared by mupen64plus converter as well, so only old old mupen ever sees this trick. - - //update: I stumbled upon a .st that had the bit set, but didn't have SI_INT in queue, - //so it froze game, so there exists a way to cause that somehow - if (get_event(SI_INT) == 0) //if there is no interrupt, add it, otherwise dont care - { - g_core_logger->warn("[ST] No SI interrupt in queue, adding one..."); - for (size_t i = 0; i < (64 / 4); i++) - rdram[si_register.si_dram_addr / 4 + i] = sl(PIF_RAM[i]); - update_count(); - add_interrupt_event(SI_INT, /*0x100*/0x900); - rdram_register.rdram_device_manuf |= RDRAM_DEVICE_MANUF_NEW_FIX_BIT; - g_st_skip_dma = true; - } - //hack end - } - - // NOTE: This saving needs to be done **after** the fixing block, as it is now. See previous regression in f9d58f639c798cbc26bbb808b1c3dbd834ffe2d9. - save_flashram_infos(g_flashram_buf); - const int32_t event_queue_len = save_eventqueue_infos(g_event_queue_buf); - - vecwrite(b, rom_md5, 32); - vecwrite(b, &rdram_register, sizeof(RDRAM_register)); - vecwrite(b, &MI_register, sizeof(mips_register)); - vecwrite(b, &pi_register, sizeof(PI_register)); - vecwrite(b, &sp_register, sizeof(SP_register)); - vecwrite(b, &rsp_register, sizeof(RSP_register)); - vecwrite(b, &si_register, sizeof(SI_register)); - vecwrite(b, &vi_register, sizeof(VI_register)); - vecwrite(b, &ri_register, sizeof(RI_register)); - vecwrite(b, &ai_register, sizeof(AI_register)); - vecwrite(b, &dpc_register, sizeof(DPC_register)); - vecwrite(b, &dps_register, sizeof(DPS_register)); - vecwrite(b, rdram, 0x800000); - vecwrite(b, SP_DMEM, 0x1000); - vecwrite(b, SP_IMEM, 0x1000); - vecwrite(b, PIF_RAM, 0x40); - vecwrite(b, g_flashram_buf, 24); - vecwrite(b, tlb_LUT_r, 0x100000); - vecwrite(b, tlb_LUT_w, 0x100000); - vecwrite(b, &llbit, 4); - vecwrite(b, reg, 32 * 8); - for (size_t i = 0; i < 32; i++) - vecwrite(b, reg_cop0 + i, 8); // *8 for compatibility with old versions purpose - vecwrite(b, &lo, 8); - vecwrite(b, &hi, 8); - vecwrite(b, reg_cop1_fgr_64, 32 * 8); - vecwrite(b, &FCR0, 4); - vecwrite(b, &FCR31, 4); - vecwrite(b, tlb_e, 32 * sizeof(tlb)); - if (!dynacore && interpcore) - vecwrite(b, &interp_addr, 4); - else - vecwrite(b, &PC->addr, 4); - vecwrite(b, &next_interrupt, 4); - vecwrite(b, &next_vi, 4); - vecwrite(b, &vi_field, 4); - vecwrite(b, g_event_queue_buf, event_queue_len); - vecwrite(b, &movie_active, sizeof(movie_active)); - if (movie_active) - { - auto movie_freeze = VCR::freeze().value(); - - vecwrite(b, &movie_freeze.size, sizeof(movie_freeze.size)); - vecwrite(b, &movie_freeze.uid, sizeof(movie_freeze.uid)); - vecwrite(b, &movie_freeze.current_sample, sizeof(movie_freeze.current_sample)); - vecwrite(b, &movie_freeze.current_vi, sizeof(movie_freeze.current_vi)); - vecwrite(b, &movie_freeze.length_samples, sizeof(movie_freeze.length_samples)); - vecwrite(b, movie_freeze.input_buffer.data(), movie_freeze.input_buffer.size() * sizeof(BUTTONS)); - } - - if (is_mge_available() && g_config.st_screenshot) - { - int32_t width; - int32_t height; - FrontendService::mge_get_video_size(&width, &height); - - void* video = malloc(width * height * 3); - FrontendService::mge_copy_video(video); - - vecwrite(b, screen_section, sizeof(screen_section)); - vecwrite(b, &width, sizeof(width)); - vecwrite(b, &height, sizeof(height)); - vecwrite(b, video, width * height * 3); - - free(video); - } - - return b; - } - - void savestates_save_immediate_impl(const t_savestate_task& task) - { - ScopeTimer timer("Savestate saving", g_core_logger); - - const auto st = generate_savestate(); - - if (task.medium == Medium::Slot || task.medium == Medium::Path) - { - // Always save summercart for some reason - std::filesystem::path new_st_path = task.params.path; - std::filesystem::path new_sd_path = ""; - get_paths_for_task(task, new_st_path, new_sd_path); - if (g_config.use_summercart) save_summercart(new_sd_path.string().c_str()); - - // Generate compressed buffer - std::vector compressed_buffer; - compressed_buffer.resize(st.size()); - - const auto compressor = libdeflate_alloc_compressor(6); - const size_t final_size = libdeflate_gzip_compress(compressor, st.data(), st.size(), compressed_buffer.data(), compressed_buffer.size()); - libdeflate_free_compressor(compressor); - compressed_buffer.resize(final_size); - - // write compressed st to disk - FILE* f = fopen(new_st_path.string().c_str(), "wb"); - - if (f == nullptr) - { - task.callback(CoreResult::ST_FileWriteError, st); - return; - } - - fwrite(compressed_buffer.data(), compressed_buffer.size(), 1, f); - fclose(f); - } - - task.callback(CoreResult::Ok, st); - LuaService::call_save_state(); - } - - void savestates_load_immediate_impl(const t_savestate_task& task) - { - ScopeTimer timer("Savestate loading", g_core_logger); - - memset(g_event_queue_buf, 0, sizeof(g_event_queue_buf)); - - std::filesystem::path new_st_path = task.params.path; - std::filesystem::path new_sd_path = ""; - get_paths_for_task(task, new_st_path, new_sd_path); - - if (g_config.use_summercart) load_summercart(new_sd_path.string().c_str()); - - std::vector st_buf; - - switch (task.medium) - { - case Medium::Slot: - case Medium::Path: - st_buf = read_file_buffer(new_st_path); - break; - case Medium::Memory: - st_buf = task.params.buffer; - break; - default: assert(false); - } - - if (st_buf.empty()) - { - task.callback(CoreResult::ST_NotFound, {}); - return; - } - - std::vector decompressed_buf = auto_decompress(st_buf); - if (decompressed_buf.empty()) - { - task.callback(CoreResult::ST_DecompressionError, {}); - return; - } - - // BUG (PRONE): we arent allowed to hold on to a vector element pointer - // find another way of doing this - auto ptr = decompressed_buf.data(); - - // compare current rom hash with one stored in state - char md5[33] = {0}; - memread(&ptr, &md5, 32); - - if (!task.ignore_warnings && memcmp(md5, rom_md5, 32)) - { - auto result = FrontendService::show_ask_dialog(std::format( - L"The savestate was created on a rom with hash {}, but is being loaded on another rom.\r\nThe emulator may crash. Are you sure you want to continue?", - string_to_wstring(md5)).c_str()); - - if (!result) - { - task.callback(CoreResult::ST_Cancelled, {}); - return; - } - } - - // new version does one bigass gzread for first part of .st (static size) - memread(&ptr, g_first_block, sizeof(g_first_block)); - - // now read interrupt queue into buf - int32_t len; - for (len = 0; len < sizeof(g_event_queue_buf); len += 8) - { - memread(&ptr, g_event_queue_buf + len, 4); - if (*reinterpret_cast(&g_event_queue_buf[len]) == 0xFFFFFFFF) - break; - memread(&ptr, g_event_queue_buf + len + 4, 4); - } - if (len == sizeof(g_event_queue_buf)) - { - // Exhausted the buffer and still no terminator. Prevents the buffer overflow "Queuecrush". - task.callback(CoreResult::ST_EventQueueTooLong, {}); - return; - } - - uint32_t is_movie; - memread(&ptr, &is_movie, sizeof(is_movie)); - - if (is_movie) - { - // this .st is part of a movie, we need to overwrite our current movie buffer - // hash matches, load and verify rest of the data - t_movie_freeze freeze{}; - - memread(&ptr, &freeze.size, sizeof(freeze.size)); - memread(&ptr, &freeze.uid, sizeof(freeze.uid)); - memread(&ptr, &freeze.current_sample, sizeof(freeze.current_sample)); - memread(&ptr, &freeze.current_vi, sizeof(freeze.current_vi)); - memread(&ptr, &freeze.length_samples, sizeof(freeze.length_samples)); - - freeze.input_buffer.resize(sizeof(BUTTONS) * (freeze.length_samples + 1)); - memread(&ptr, freeze.input_buffer.data(), freeze.input_buffer.size()); - - const auto code = VCR::unfreeze(freeze); - - if (!task.ignore_warnings && code != CoreResult::Ok && VCR::get_task() != e_task::idle) - { - std::wstring err_str = L"Failed to restore movie, "; - switch (code) - { - case CoreResult::VCR_NotFromThisMovie: - err_str += L"the savestate is not from this movie."; - break; - case CoreResult::VCR_InvalidFrame: - err_str += L"the savestate frame is outside the bounds of the movie."; - break; - case CoreResult::VCR_InvalidFormat: - err_str += L"the savestate freeze buffer format is invalid."; - break; - default: - err_str += L"an unknown error has occured."; - break; - } - err_str += L" Loading the savestate might desynchronize the movie.\r\nAre you sure you want to continue?"; - - const auto result = FrontendService::show_ask_dialog(err_str.c_str(), L"Savestate", true); - if (!result) - { - task.callback(CoreResult::ST_Cancelled, {}); - goto failedLoad; - } - } - } - else - { - if (!task.ignore_warnings && (VCR::get_task() == e_task::recording || VCR::get_task() == e_task::playback)) - { - const auto result = FrontendService::show_ask_dialog(L"The savestate is not from a movie. Loading it might desynchronize the movie.\r\nAre you sure you want to continue?", L"Savestate", true); - if (!result) - { - task.callback(CoreResult::ST_Cancelled, {}); - return; - } - } - - // at this point we know the savestate is safe to be loaded (done after else block) - } - { - g_core_logger->info("[Savestates] {} bytes remaining", decompressed_buf.size() - (ptr - decompressed_buf.data())); - int32_t video_width = 0; - int32_t video_height = 0; - void* video_buffer = nullptr; - if (decompressed_buf.size() - (ptr - decompressed_buf.data()) > 0) - { - char scr_section[sizeof(screen_section)] = {0}; - memread(&ptr, scr_section, sizeof(screen_section)); - - if (!memcmp(scr_section, screen_section, sizeof(screen_section))) - { - g_core_logger->info("[Savestates] Restoring screen buffer..."); - memread(&ptr, &video_width, sizeof(video_width)); - memread(&ptr, &video_height, sizeof(video_height)); - - video_buffer = malloc(video_width * video_height * 3); - memread(&ptr, video_buffer, video_width * video_height * 3); - } - } - - // so far loading success! overwrite memory - load_eventqueue_infos(g_event_queue_buf); - load_memory_from_buffer(g_first_block); - - // NOTE: We don't want to restore screen buffer while seeking, since it creates a int16_t ugly flicker when the movie restarts by loading state - if (is_mge_available() && video_buffer && !VCR::is_seeking()) - { - int32_t current_width, current_height; - FrontendService::mge_get_video_size(¤t_width, ¤t_height); - if (current_width == video_width && current_height == video_height) - { - FrontendService::mge_load_screen(video_buffer); - free(video_buffer); - } - } - } - - LuaService::call_load_state(); - task.callback(CoreResult::Ok, decompressed_buf); - - failedLoad: - //legacy .st fix, makes BEQ instruction ignore jump, because .st writes new address explictly. - //This should cause issues anyway but libultra seems to be flexible (this means there's a chance it fails). - //For safety, load .sts in dynarec because it completely avoids this issue by being differently coded - g_st_old = (interp_addr == 0x80000180 || PC->addr == 0x80000180); - //doubled because can't just reuse this variable - if (interp_addr == 0x80000180 || (PC->addr == 0x80000180 && !dynacore)) - g_vr_beq_ignore_jmp = true; - if (!dynacore && interpcore) - { - //g_core_logger->info(".st jump: {:#06x}, stopped here:{:#06x}", interp_addr, last_addr); - last_addr = interp_addr; - } - else - { - //g_core_logger->info(".st jump: {:#06x}, stopped here:{:#06x}", PC->addr, last_addr); - last_addr = PC->addr; - } - } - - /** - * Simplifies the task queue by removing duplicates. Only slot-based tasks are affected for now. - */ - void savestates_simplify_tasks() - { - std::scoped_lock lock(g_task_mutex); - g_core_logger->info("[ST] Simplifying task queue..."); - - std::vector duplicate_indicies{}; - - - // De-dup slot-based save tasks - // 1. Loop through all tasks - for (size_t i = 0; i < g_tasks.size(); i++) - { - const auto& task = g_tasks[i]; - - if (task.medium != Medium::Slot) - continue; - - // 2. If a slot task is detected, loop through all other tasks up to the next load task to find duplicates - for (size_t j = i + 1; j < g_tasks.size(); j++) - { - const auto& other_task = g_tasks[j]; - - if (other_task.job == Job::Load) - { - break; - } - - if (other_task.medium == Medium::Slot && task.params.slot == other_task.params.slot) - { - g_core_logger->info("[ST] Found duplicate slot task at index {}", j); - duplicate_indicies.push_back(j); - } - } - } - - g_tasks = erase_indices(g_tasks, duplicate_indicies); - } - - /** - * Logs the current task queue. - */ - void savestates_log_tasks() - { - std::scoped_lock lock(g_task_mutex); - g_core_logger->info("[ST] Begin task dump"); - for (const auto& task : g_tasks) - { - std::string job_str = (task.job == Job::Save) ? "Save" : "Load"; - std::string medium_str; - switch (task.medium) - { - case Medium::Slot: - medium_str = "Slot"; - break; - case Medium::Path: - medium_str = "Path"; - break; - case Medium::Memory: - medium_str = "Memory"; - break; - default: - medium_str = "Unknown"; - break; - } - g_core_logger->info("[ST] \tTask: Job = {}, Medium = {}", job_str, medium_str); - } - g_core_logger->info("[ST] End task dump"); - } - - /** + /// Represents a task to be performed by the savestate system. + struct t_savestate_task + { + struct t_params + { + /// The savestate slot. + /// Valid if the task's medium is . + size_t slot; + + /// The path to the savestate file. + /// Valid if the task's medium is . + std::filesystem::path path; + + /// The buffer containing the savestate data. + /// Valid if the task's medium is and the job is . + std::vector buffer; + }; + + /// The job to perform. + Job job; + + /// The savestate's source or target medium. + Medium medium; + + /// Callback to invoke when the task finishes. Mustn't be null. + t_savestate_callback callback; + + /// The task's parameters. Only one field in the struct is valid at a time. + t_params params{}; + + /// Whether warnings, such as those about ROM compatibility, shouldn't be shown. + bool ignore_warnings; + }; + + // Enable fixing .st to work for old mupen and m64p + constexpr bool FIX_NEW_ST = true; + + // Bit to set in RDRAM register to indicate that the savestate has been fixed + constexpr auto RDRAM_DEVICE_MANUF_NEW_FIX_BIT = (1 << 31); + + // The task vector mutex. Locked when accessing the task vector. + std::recursive_mutex g_task_mutex; + + // The task vector, which contains the task queue to be performed by the savestate system. + std::vector g_tasks; + + // Demarcator for new screenshot section + char screen_section[] = "SCR"; + + // Buffer used for storing flashram data during loading + char g_flashram_buf[1024]{}; + + // Buffer used for storing event queue data during loading + char g_event_queue_buf[1024]{}; + + // Buffer used for storing st data up to event queue + uint8_t g_first_block[0xA02BB4 - 32]{}; + + // The undo savestate buffer. + std::vector g_undo_savestate; + + void get_paths_for_task(const t_savestate_task& task, std::filesystem::path& st_path, std::filesystem::path& sd_path) + { + sd_path = std::format("{}{}.sd", get_saves_directory().string(), (const char*)ROM_HEADER.nom); + + if (task.medium == Medium::Slot) + { + st_path = std::format( + L"{}{} {}.st{}", + get_saves_directory().wstring(), + string_to_wstring((const char*)ROM_HEADER.nom), + country_code_to_country_name(ROM_HEADER.Country_code), std::to_wstring(task.params.slot)); + } + } + + void init() + { + } + + void load_memory_from_buffer(uint8_t* p) + { + memread(&p, &rdram_register, sizeof(RDRAM_register)); + if (rdram_register.rdram_device_manuf & RDRAM_DEVICE_MANUF_NEW_FIX_BIT) + { + rdram_register.rdram_device_manuf &= ~RDRAM_DEVICE_MANUF_NEW_FIX_BIT; //remove the trick + g_st_skip_dma = true; //tell dma.c to skip it + } + memread(&p, &MI_register, sizeof(mips_register)); + memread(&p, &pi_register, sizeof(PI_register)); + memread(&p, &sp_register, sizeof(SP_register)); + memread(&p, &rsp_register, sizeof(RSP_register)); + memread(&p, &si_register, sizeof(SI_register)); + memread(&p, &vi_register, sizeof(VI_register)); + memread(&p, &ri_register, sizeof(RI_register)); + memread(&p, &ai_register, sizeof(AI_register)); + memread(&p, &dpc_register, sizeof(DPC_register)); + memread(&p, &dps_register, sizeof(DPS_register)); + memread(&p, rdram, 0x800000); + memread(&p, SP_DMEM, 0x1000); + memread(&p, SP_IMEM, 0x1000); + memread(&p, PIF_RAM, 0x40); + + char buf[4 * 32]; + memread(&p, buf, 24); + load_flashram_infos(buf); + + memread(&p, tlb_LUT_r, 0x100000); + memread(&p, tlb_LUT_w, 0x100000); + + memread(&p, &llbit, 4); + memread(&p, reg, 32 * 8); + for (int32_t i = 0; i < 32; i++) + { + memread(&p, reg_cop0 + i, 4); + memread(&p, buf, 4); // for compatibility with old versions purpose + } + memread(&p, &lo, 8); + memread(&p, &hi, 8); + memread(&p, reg_cop1_fgr_64, 32 * 8); + memread(&p, &FCR0, 4); + memread(&p, &FCR31, 4); + memread(&p, tlb_e, 32 * sizeof(tlb)); + if (!dynacore && interpcore) + memread(&p, &interp_addr, 4); + else + { + uint32_t target_addr; + memread(&p, &target_addr, 4); + for (char& i : invalid_code) + i = 1; + jump_to(target_addr) + } + + memread(&p, &next_interrupt, 4); + memread(&p, &next_vi, 4); + memread(&p, &vi_field, 4); + } + + std::vector generate_savestate() + { + std::vector b; + + b.reserve(0xB624F0); + + memset(g_flashram_buf, 0, sizeof(g_flashram_buf)); + memset(g_event_queue_buf, 0, sizeof(g_event_queue_buf)); + + uint32_t movie_active = VCR::get_task() != e_task::idle; + + if (FIX_NEW_ST) + { + //this is code taken from dma.c:dma_si_read(), it finishes up the dma. + //it copies data from pif (should contain commands and controller states), updates count reg and adds SI interrupt to queue + //so why does old mupen and mupen64plus dislike savestates without doing this? well in case of mario 64 it leaves pif command buffer uninitialised + //and it never can poll input properly (hence the inability to frame advance which is handled inside controller read). + + //But we dont want to do this then load such .st and dma again... so I notify mupen about this in .st, + //since .st is filled up to the brim with data (not even a single unused offset) I have to store one bit in... rdram manufacturer register + //this 99.999% wont break on any game, and that bit will be cleared by mupen64plus converter as well, so only old old mupen ever sees this trick. + + //update: I stumbled upon a .st that had the bit set, but didn't have SI_INT in queue, + //so it froze game, so there exists a way to cause that somehow + if (get_event(SI_INT) == 0) //if there is no interrupt, add it, otherwise dont care + { + g_core_logger->warn("[ST] No SI interrupt in queue, adding one..."); + for (size_t i = 0; i < (64 / 4); i++) + rdram[si_register.si_dram_addr / 4 + i] = sl(PIF_RAM[i]); + update_count(); + add_interrupt_event(SI_INT, /*0x100*/0x900); + rdram_register.rdram_device_manuf |= RDRAM_DEVICE_MANUF_NEW_FIX_BIT; + g_st_skip_dma = true; + } + //hack end + } + + // NOTE: This saving needs to be done **after** the fixing block, as it is now. See previous regression in f9d58f639c798cbc26bbb808b1c3dbd834ffe2d9. + save_flashram_infos(g_flashram_buf); + const int32_t event_queue_len = save_eventqueue_infos(g_event_queue_buf); + + vecwrite(b, rom_md5, 32); + vecwrite(b, &rdram_register, sizeof(RDRAM_register)); + vecwrite(b, &MI_register, sizeof(mips_register)); + vecwrite(b, &pi_register, sizeof(PI_register)); + vecwrite(b, &sp_register, sizeof(SP_register)); + vecwrite(b, &rsp_register, sizeof(RSP_register)); + vecwrite(b, &si_register, sizeof(SI_register)); + vecwrite(b, &vi_register, sizeof(VI_register)); + vecwrite(b, &ri_register, sizeof(RI_register)); + vecwrite(b, &ai_register, sizeof(AI_register)); + vecwrite(b, &dpc_register, sizeof(DPC_register)); + vecwrite(b, &dps_register, sizeof(DPS_register)); + vecwrite(b, rdram, 0x800000); + vecwrite(b, SP_DMEM, 0x1000); + vecwrite(b, SP_IMEM, 0x1000); + vecwrite(b, PIF_RAM, 0x40); + vecwrite(b, g_flashram_buf, 24); + vecwrite(b, tlb_LUT_r, 0x100000); + vecwrite(b, tlb_LUT_w, 0x100000); + vecwrite(b, &llbit, 4); + vecwrite(b, reg, 32 * 8); + for (size_t i = 0; i < 32; i++) + vecwrite(b, reg_cop0 + i, 8); // *8 for compatibility with old versions purpose + vecwrite(b, &lo, 8); + vecwrite(b, &hi, 8); + vecwrite(b, reg_cop1_fgr_64, 32 * 8); + vecwrite(b, &FCR0, 4); + vecwrite(b, &FCR31, 4); + vecwrite(b, tlb_e, 32 * sizeof(tlb)); + if (!dynacore && interpcore) + vecwrite(b, &interp_addr, 4); + else + vecwrite(b, &PC->addr, 4); + vecwrite(b, &next_interrupt, 4); + vecwrite(b, &next_vi, 4); + vecwrite(b, &vi_field, 4); + vecwrite(b, g_event_queue_buf, event_queue_len); + vecwrite(b, &movie_active, sizeof(movie_active)); + if (movie_active) + { + auto movie_freeze = VCR::freeze().value(); + + vecwrite(b, &movie_freeze.size, sizeof(movie_freeze.size)); + vecwrite(b, &movie_freeze.uid, sizeof(movie_freeze.uid)); + vecwrite(b, &movie_freeze.current_sample, sizeof(movie_freeze.current_sample)); + vecwrite(b, &movie_freeze.current_vi, sizeof(movie_freeze.current_vi)); + vecwrite(b, &movie_freeze.length_samples, sizeof(movie_freeze.length_samples)); + vecwrite(b, movie_freeze.input_buffer.data(), movie_freeze.input_buffer.size() * sizeof(BUTTONS)); + } + + if (is_mge_available() && g_config.st_screenshot) + { + int32_t width; + int32_t height; + FrontendService::mge_get_video_size(&width, &height); + + void* video = malloc(width * height * 3); + FrontendService::mge_copy_video(video); + + vecwrite(b, screen_section, sizeof(screen_section)); + vecwrite(b, &width, sizeof(width)); + vecwrite(b, &height, sizeof(height)); + vecwrite(b, video, width * height * 3); + + free(video); + } + + return b; + } + + void savestates_save_immediate_impl(const t_savestate_task& task) + { + ScopeTimer timer("Savestate saving", g_core_logger); + + const auto st = generate_savestate(); + + if (task.medium == Medium::Slot || task.medium == Medium::Path) + { + // Always save summercart for some reason + std::filesystem::path new_st_path = task.params.path; + std::filesystem::path new_sd_path = ""; + get_paths_for_task(task, new_st_path, new_sd_path); + if (g_config.use_summercart) save_summercart(new_sd_path.string().c_str()); + + // Generate compressed buffer + std::vector compressed_buffer; + compressed_buffer.resize(st.size()); + + const auto compressor = libdeflate_alloc_compressor(6); + const size_t final_size = libdeflate_gzip_compress(compressor, st.data(), st.size(), compressed_buffer.data(), compressed_buffer.size()); + libdeflate_free_compressor(compressor); + compressed_buffer.resize(final_size); + + // write compressed st to disk + FILE* f = fopen(new_st_path.string().c_str(), "wb"); + + if (f == nullptr) + { + task.callback(CoreResult::ST_FileWriteError, st); + return; + } + + fwrite(compressed_buffer.data(), compressed_buffer.size(), 1, f); + fclose(f); + } + + task.callback(CoreResult::Ok, st); + LuaService::call_save_state(); + } + + void savestates_load_immediate_impl(const t_savestate_task& task) + { + ScopeTimer timer("Savestate loading", g_core_logger); + + memset(g_event_queue_buf, 0, sizeof(g_event_queue_buf)); + + std::filesystem::path new_st_path = task.params.path; + std::filesystem::path new_sd_path = ""; + get_paths_for_task(task, new_st_path, new_sd_path); + + if (g_config.use_summercart) load_summercart(new_sd_path.string().c_str()); + + std::vector st_buf; + + switch (task.medium) + { + case Medium::Slot: + case Medium::Path: + st_buf = read_file_buffer(new_st_path); + break; + case Medium::Memory: + st_buf = task.params.buffer; + break; + default: assert(false); + } + + if (st_buf.empty()) + { + task.callback(CoreResult::ST_NotFound, {}); + return; + } + + std::vector decompressed_buf = auto_decompress(st_buf); + if (decompressed_buf.empty()) + { + task.callback(CoreResult::ST_DecompressionError, {}); + return; + } + + // BUG (PRONE): we arent allowed to hold on to a vector element pointer + // find another way of doing this + auto ptr = decompressed_buf.data(); + + // compare current rom hash with one stored in state + char md5[33] = {0}; + memread(&ptr, &md5, 32); + + if (!task.ignore_warnings && memcmp(md5, rom_md5, 32)) + { + auto result = FrontendService::show_ask_dialog(std::format( + L"The savestate was created on a rom with hash {}, but is being loaded on another rom.\r\nThe emulator may crash. Are you sure you want to continue?", + string_to_wstring(md5)).c_str()); + + if (!result) + { + task.callback(CoreResult::ST_Cancelled, {}); + return; + } + } + + // new version does one bigass gzread for first part of .st (static size) + memread(&ptr, g_first_block, sizeof(g_first_block)); + + // now read interrupt queue into buf + int32_t len; + for (len = 0; len < sizeof(g_event_queue_buf); len += 8) + { + memread(&ptr, g_event_queue_buf + len, 4); + if (*reinterpret_cast(&g_event_queue_buf[len]) == 0xFFFFFFFF) + break; + memread(&ptr, g_event_queue_buf + len + 4, 4); + } + if (len == sizeof(g_event_queue_buf)) + { + // Exhausted the buffer and still no terminator. Prevents the buffer overflow "Queuecrush". + task.callback(CoreResult::ST_EventQueueTooLong, {}); + return; + } + + uint32_t is_movie; + memread(&ptr, &is_movie, sizeof(is_movie)); + + if (is_movie) + { + // this .st is part of a movie, we need to overwrite our current movie buffer + // hash matches, load and verify rest of the data + t_movie_freeze freeze{}; + + memread(&ptr, &freeze.size, sizeof(freeze.size)); + memread(&ptr, &freeze.uid, sizeof(freeze.uid)); + memread(&ptr, &freeze.current_sample, sizeof(freeze.current_sample)); + memread(&ptr, &freeze.current_vi, sizeof(freeze.current_vi)); + memread(&ptr, &freeze.length_samples, sizeof(freeze.length_samples)); + + freeze.input_buffer.resize(sizeof(BUTTONS) * (freeze.length_samples + 1)); + memread(&ptr, freeze.input_buffer.data(), freeze.input_buffer.size()); + + const auto code = VCR::unfreeze(freeze); + + if (!task.ignore_warnings && code != CoreResult::Ok && VCR::get_task() != e_task::idle) + { + std::wstring err_str = L"Failed to restore movie, "; + switch (code) + { + case CoreResult::VCR_NotFromThisMovie: + err_str += L"the savestate is not from this movie."; + break; + case CoreResult::VCR_InvalidFrame: + err_str += L"the savestate frame is outside the bounds of the movie."; + break; + case CoreResult::VCR_InvalidFormat: + err_str += L"the savestate freeze buffer format is invalid."; + break; + default: + err_str += L"an unknown error has occured."; + break; + } + err_str += L" Loading the savestate might desynchronize the movie.\r\nAre you sure you want to continue?"; + + const auto result = FrontendService::show_ask_dialog(err_str.c_str(), L"Savestate", true); + if (!result) + { + task.callback(CoreResult::ST_Cancelled, {}); + goto failedLoad; + } + } + } else + { + if (!task.ignore_warnings && (VCR::get_task() == e_task::recording || VCR::get_task() == e_task::playback)) + { + const auto result = FrontendService::show_ask_dialog( + L"The savestate is not from a movie. Loading it might desynchronize the movie.\r\nAre you sure you want to continue?", L"Savestate", true); + if (!result) + { + task.callback(CoreResult::ST_Cancelled, {}); + return; + } + } + + // at this point we know the savestate is safe to be loaded (done after else block) + } + { + g_core_logger->info("[Savestates] {} bytes remaining", decompressed_buf.size() - (ptr - decompressed_buf.data())); + int32_t video_width = 0; + int32_t video_height = 0; + void* video_buffer = nullptr; + if (decompressed_buf.size() - (ptr - decompressed_buf.data()) > 0) + { + char scr_section[sizeof(screen_section)] = {0}; + memread(&ptr, scr_section, sizeof(screen_section)); + + if (!memcmp(scr_section, screen_section, sizeof(screen_section))) + { + g_core_logger->info("[Savestates] Restoring screen buffer..."); + memread(&ptr, &video_width, sizeof(video_width)); + memread(&ptr, &video_height, sizeof(video_height)); + + video_buffer = malloc(video_width * video_height * 3); + memread(&ptr, video_buffer, video_width * video_height * 3); + } + } + + // so far loading success! overwrite memory + load_eventqueue_infos(g_event_queue_buf); + load_memory_from_buffer(g_first_block); + + // NOTE: We don't want to restore screen buffer while seeking, since it creates a int16_t ugly flicker when the movie restarts by loading state + if (is_mge_available() && video_buffer && !VCR::is_seeking()) + { + int32_t current_width, current_height; + FrontendService::mge_get_video_size(¤t_width, ¤t_height); + if (current_width == video_width && current_height == video_height) + { + FrontendService::mge_load_screen(video_buffer); + free(video_buffer); + } + } + } + + LuaService::call_load_state(); + task.callback(CoreResult::Ok, decompressed_buf); + + failedLoad: + //legacy .st fix, makes BEQ instruction ignore jump, because .st writes new address explictly. + //This should cause issues anyway but libultra seems to be flexible (this means there's a chance it fails). + //For safety, load .sts in dynarec because it completely avoids this issue by being differently coded + g_st_old = (interp_addr == 0x80000180 || PC->addr == 0x80000180); + //doubled because can't just reuse this variable + if (interp_addr == 0x80000180 || (PC->addr == 0x80000180 && !dynacore)) + g_vr_beq_ignore_jmp = true; + if (!dynacore && interpcore) + { + //g_core_logger->info(".st jump: {:#06x}, stopped here:{:#06x}", interp_addr, last_addr); + last_addr = interp_addr; + } else + { + //g_core_logger->info(".st jump: {:#06x}, stopped here:{:#06x}", PC->addr, last_addr); + last_addr = PC->addr; + } + } + + /** + * Simplifies the task queue by removing duplicates. Only slot-based tasks are affected for now. + */ + void savestates_simplify_tasks() + { + std::scoped_lock lock(g_task_mutex); + g_core_logger->info("[ST] Simplifying task queue..."); + + std::vector duplicate_indicies{}; + + + // De-dup slot-based save tasks + // 1. Loop through all tasks + for (size_t i = 0; i < g_tasks.size(); i++) + { + const auto& task = g_tasks[i]; + + if (task.medium != Medium::Slot) + continue; + + // 2. If a slot task is detected, loop through all other tasks up to the next load task to find duplicates + for (size_t j = i + 1; j < g_tasks.size(); j++) + { + const auto& other_task = g_tasks[j]; + + if (other_task.job == Job::Load) + { + break; + } + + if (other_task.medium == Medium::Slot && task.params.slot == other_task.params.slot) + { + g_core_logger->info("[ST] Found duplicate slot task at index {}", j); + duplicate_indicies.push_back(j); + } + } + } + + g_tasks = erase_indices(g_tasks, duplicate_indicies); + } + + /** * Warns if a savestate load task is scheduled after a save task. */ - void savestates_warn_if_load_after_save() - { - std::scoped_lock lock(g_task_mutex); - - bool encountered_load = false; - for (const auto& task : g_tasks) - { - if (task.job == Job::Save && encountered_load) - { - g_core_logger->warn("[ST] A savestate save task is scheduled after a load task. This may cause unexpected behavior for the caller."); - break; - } - - if (task.job == Job::Load) - { - encountered_load = true; - } - } - } - - void do_work() - { - std::scoped_lock lock(g_task_mutex); - - if (g_tasks.empty()) - { - return; - } - - g_core_logger->info("[ST] Processing {} tasks...", g_tasks.size()); - savestates_simplify_tasks(); - savestates_log_tasks(); - - for (const auto& task : g_tasks) - { - if (task.job == Job::Save) - { - savestates_save_immediate_impl(task); - } - else - { - savestates_load_immediate_impl(task); - } - } - g_tasks.clear(); - } - - void clear_work() - { - std::scoped_lock lock(g_task_mutex); - g_core_logger->trace("[ST] Clearing {} tasks...", g_tasks.size()); - g_tasks.clear(); - } - - /** - * Gets whether work can currently be enqueued. - */ - bool can_push_work() - { - return emu_launched; - } - - void do_file(const std::filesystem::path& path, const Job job, const t_savestate_callback& callback, bool ignore_warnings) - { - if (!can_push_work()) - { - g_core_logger->trace("[ST] do_file: Can't enqueue work."); - if (callback) - { - callback(CoreResult::ST_CoreNotLaunched, {}); - } - return; - } - - std::scoped_lock lock(g_task_mutex); - - auto pre_callback = [=](const CoreResult result, const std::vector& buffer) - { - if (result == CoreResult::Ok) - { - FrontendService::show_statusbar(std::format(L"{} {}", job == Job::Save ? L"Saved" : L"Loaded", path.filename().wstring()).c_str()); - } - else if (result == CoreResult::ST_Cancelled) - { - FrontendService::show_statusbar(std::format(L"Cancelled {}", job == Job::Save ? L"save" : L"load").c_str()); - } - else - { - FrontendService::show_error(std::format(L"Failed to {} {} (error code {}).\nVerify that the savestate is valid and accessible.", job == Job::Save ? L"save" : L"load", path.filename().wstring(), (int32_t)result).c_str(), L"Savestate"); - } - - if (callback) - { - callback(result, buffer); - } - }; - - const t_savestate_task task = { - .job = job, - .medium = Medium::Path, - .callback = pre_callback, - .params = { - .path = path - }, - .ignore_warnings = ignore_warnings, - }; - - g_tasks.insert(g_tasks.begin(), task); - savestates_warn_if_load_after_save(); - } - - void do_slot(const int32_t slot, const Job job, const t_savestate_callback& callback, bool ignore_warnings) - { - if (!can_push_work()) - { - g_core_logger->trace("[ST] do_slot: Can't enqueue work."); - if (callback) - { - callback(CoreResult::ST_CoreNotLaunched, {}); - } - return; - } - - std::scoped_lock lock(g_task_mutex); - - if (g_config.increment_slot && job == Job::Save) - { - g_config.st_slot >= 9 ? g_config.st_slot = 0 : g_config.st_slot++; - Messenger::broadcast(Messenger::Message::SlotChanged, (size_t)g_config.st_slot); - } - - auto pre_callback = [=](const CoreResult result, const std::vector& buffer) - { - if (result == CoreResult::Ok) - { - FrontendService::show_statusbar(std::format(L"{} slot {}", job == Job::Save ? L"Saved" : L"Loaded", slot + 1).c_str()); - } - else if (result == CoreResult::ST_Cancelled) - { - FrontendService::show_statusbar(std::format(L"Cancelled {}", job == Job::Save ? L"save" : L"load").c_str()); - } - else - { - FrontendService::show_statusbar(std::format(L"Failed to {} slot {}", job == Job::Save ? L"save" : L"load", slot + 1).c_str()); - } - - if (callback) - { - callback(result, buffer); - } - }; - - const t_savestate_task task = { - .job = job, - .medium = Medium::Slot, - .callback = pre_callback, - .params = { - .slot = static_cast(slot) - }, - .ignore_warnings = ignore_warnings, - }; - - g_tasks.insert(g_tasks.begin(), task); - savestates_warn_if_load_after_save(); - } - - void do_memory(const std::vector& buffer, const Job job, const t_savestate_callback& callback, bool ignore_warnings) - { - if (!can_push_work()) - { - g_core_logger->trace("[ST] do_memory: Can't enqueue work."); - if (callback) - { - callback(CoreResult::ST_CoreNotLaunched, {}); - } - return; - } - - std::scoped_lock lock(g_task_mutex); - - const t_savestate_task task = { - .job = job, - .medium = Medium::Memory, - .callback = callback, - .params = { - .buffer = buffer - }, - .ignore_warnings = ignore_warnings, - }; - - g_tasks.insert(g_tasks.begin(), task); - savestates_warn_if_load_after_save(); - } + void savestates_warn_if_load_after_save() + { + std::scoped_lock lock(g_task_mutex); + + bool encountered_load = false; + for (const auto& task : g_tasks) + { + if (task.job == Job::Save && encountered_load) + { + g_core_logger->warn("[ST] A savestate save task is scheduled after a load task. This may cause unexpected behavior for the caller."); + break; + } + + if (task.job == Job::Load) + { + encountered_load = true; + } + } + } + + /** + * Logs the current task queue. + */ + void savestates_log_tasks() + { + std::scoped_lock lock(g_task_mutex); + g_core_logger->info("[ST] Begin task dump"); + savestates_warn_if_load_after_save(); + for (const auto& task : g_tasks) + { + std::string job_str = (task.job == Job::Save) ? "Save" : "Load"; + std::string medium_str; + switch (task.medium) + { + case Medium::Slot: + medium_str = "Slot"; + break; + case Medium::Path: + medium_str = "Path"; + break; + case Medium::Memory: + medium_str = "Memory"; + break; + default: + medium_str = "Unknown"; + break; + } + g_core_logger->info("[ST] \tTask: Job = {}, Medium = {}", job_str, medium_str); + } + g_core_logger->info("[ST] End task dump"); + } + + /** + * Inserts a save operation at the start of the queue (whose callback assigns the undo savestate buffer) if the task queue contains one or more load operations. + */ + void savestates_create_undo_point() + { + if (!g_config.st_undo_load) + { + return; + } + + bool queue_contains_load = std::ranges::any_of(g_tasks, [](const t_savestate_task& task) + { + return task.job == Job::Load; + }); + + if (!queue_contains_load) + { + g_core_logger->trace("[ST] Skipping undo point creation: no load in queue."); + return; + } + + g_core_logger->trace("[ST] Inserting undo point creation into task queue..."); + + const t_savestate_task task = { + .job = Job::Save, + .medium = Medium::Memory, + .callback = [](const CoreResult result, const std::vector& buffer) + { + if (result != CoreResult::Ok) + { + return; + } + + std::scoped_lock lock(g_task_mutex); + g_undo_savestate = buffer; + }, + .params = { + .buffer = {}, + }, + .ignore_warnings = true, + }; + + g_tasks.insert(g_tasks.begin(), task); + } + + void do_work() + { + std::scoped_lock lock(g_task_mutex); + + if (g_tasks.empty()) + { + return; + } + + savestates_simplify_tasks(); + savestates_create_undo_point(); + savestates_simplify_tasks(); + savestates_log_tasks(); + + for (const auto& task : g_tasks) + { + if (task.job == Job::Save) + { + savestates_save_immediate_impl(task); + } else + { + savestates_load_immediate_impl(task); + } + } + g_tasks.clear(); + } + + void clear_work() + { + std::scoped_lock lock(g_task_mutex); + g_core_logger->trace("[ST] Clearing {} tasks...", g_tasks.size()); + g_tasks.clear(); + } + + /** + * Gets whether work can currently be enqueued. + */ + bool can_push_work() + { + return emu_launched; + } + + void do_file(const std::filesystem::path& path, const Job job, const t_savestate_callback& callback, bool ignore_warnings) + { + if (!can_push_work()) + { + g_core_logger->trace("[ST] do_file: Can't enqueue work."); + if (callback) + { + callback(CoreResult::ST_CoreNotLaunched, {}); + } + return; + } + + std::scoped_lock lock(g_task_mutex); + + auto pre_callback = [=](const CoreResult result, const std::vector& buffer) + { + if (result == CoreResult::Ok) + { + FrontendService::show_statusbar(std::format(L"{} {}", job == Job::Save ? L"Saved" : L"Loaded", path.filename().wstring()).c_str()); + } else if (result == CoreResult::ST_Cancelled) + { + FrontendService::show_statusbar(std::format(L"Cancelled {}", job == Job::Save ? L"save" : L"load").c_str()); + } else + { + FrontendService::show_error(std::format(L"Failed to {} {} (error code {}).\nVerify that the savestate is valid and accessible.", + job == Job::Save ? L"save" : L"load", path.filename().wstring(), (int32_t)result).c_str(), + L"Savestate"); + } + + if (callback) + { + callback(result, buffer); + } + }; + + const t_savestate_task task = { + .job = job, + .medium = Medium::Path, + .callback = pre_callback, + .params = { + .path = path + }, + .ignore_warnings = ignore_warnings, + }; + + g_tasks.insert(g_tasks.begin(), task); + } + + void do_slot(const int32_t slot, const Job job, const t_savestate_callback& callback, bool ignore_warnings) + { + if (!can_push_work()) + { + g_core_logger->trace("[ST] do_slot: Can't enqueue work."); + if (callback) + { + callback(CoreResult::ST_CoreNotLaunched, {}); + } + return; + } + + std::scoped_lock lock(g_task_mutex); + + if (g_config.increment_slot && job == Job::Save) + { + g_config.st_slot >= 9 ? g_config.st_slot = 0 : g_config.st_slot++; + Messenger::broadcast(Messenger::Message::SlotChanged, (size_t)g_config.st_slot); + } + + auto pre_callback = [=](const CoreResult result, const std::vector& buffer) + { + if (result == CoreResult::Ok) + { + FrontendService::show_statusbar(std::format(L"{} slot {}", job == Job::Save ? L"Saved" : L"Loaded", slot + 1).c_str()); + } else if (result == CoreResult::ST_Cancelled) + { + FrontendService::show_statusbar(std::format(L"Cancelled {}", job == Job::Save ? L"save" : L"load").c_str()); + } else + { + FrontendService::show_statusbar(std::format(L"Failed to {} slot {}", job == Job::Save ? L"save" : L"load", slot + 1).c_str()); + } + + if (callback) + { + callback(result, buffer); + } + }; + + const t_savestate_task task = { + .job = job, + .medium = Medium::Slot, + .callback = pre_callback, + .params = { + .slot = static_cast(slot) + }, + .ignore_warnings = ignore_warnings, + }; + + g_tasks.insert(g_tasks.begin(), task); + } + + void do_memory(const std::vector& buffer, const Job job, const t_savestate_callback& callback, bool ignore_warnings) + { + if (!can_push_work()) + { + g_core_logger->trace("[ST] do_memory: Can't enqueue work."); + if (callback) + { + callback(CoreResult::ST_CoreNotLaunched, {}); + } + return; + } + + std::scoped_lock lock(g_task_mutex); + + const t_savestate_task task = { + .job = job, + .medium = Medium::Memory, + .callback = callback, + .params = { + .buffer = buffer + }, + .ignore_warnings = ignore_warnings, + }; + + g_tasks.insert(g_tasks.begin(), task); + } + + std::vector get_undo_savestate() + { + std::scoped_lock lock(g_task_mutex); + return g_undo_savestate; + } } diff --git a/core/memory/savestates.h b/core/memory/savestates.h index bc34b92e..b529d717 100644 --- a/core/memory/savestates.h +++ b/core/memory/savestates.h @@ -78,7 +78,7 @@ namespace Savestates * \brief Executes a savestate operation to a path * \param path The savestate's path * \param job The job to set - * \param callback The callback to call when the operation is complete. Can be null. + * \param callback The callback to call when the operation is complete. * \param ignore_warnings Whether warnings, such as those about ROM compatibility, shouldn't be shown. * \warning The operation won't complete immediately. Must be called via AsyncExecutor unless calls are originating from the emu thread. */ @@ -88,7 +88,7 @@ namespace Savestates * \brief Executes a savestate operation to a slot * \param slot The slot to construct the savestate path with. * \param job The job to set - * \param callback The callback to call when the operation is complete. Can be null. + * \param callback The callback to call when the operation is complete. * \param ignore_warnings Whether warnings, such as those about ROM compatibility, shouldn't be shown. * \warning The operation won't complete immediately. Must be called via AsyncExecutor unless calls are originating from the emu thread. */ @@ -98,9 +98,15 @@ namespace Savestates * Executes a savestate operation in-memory. * \param buffer The buffer to use for the operation. Can be empty if the is . * \param job The job to set. - * \param callback The callback to call when the operation is complete. Can be null. + * \param callback The callback to call when the operation is complete. * \param ignore_warnings Whether warnings, such as those about ROM compatibility, shouldn't be shown. * \warning The operation won't complete immediately. Must be called via AsyncExecutor unless calls are originating from the emu thread. */ void do_memory(const std::vector& buffer, Job job, const t_savestate_callback& callback = nullptr, bool ignore_warnings = false); + + /** + * Gets the undo savestate buffer. Will be empty will no undo savestate is available. + */ + std::vector get_undo_savestate(); + } diff --git a/shared/Config.cpp b/shared/Config.cpp index 0b3c164d..72cc0df2 100644 --- a/shared/Config.cpp +++ b/shared/Config.cpp @@ -176,6 +176,11 @@ t_config get_default_config() .down_cmd = Action::LoadAs, }; + config.undo_load_state_hotkey = { + .identifier = L"Undo load state", + .down_cmd = Action::UndoLoadState, + }; + config.save_to_slot_1_hotkey = { .identifier = L"Save to slot 1", .down_cmd = Action::SaveSlot1, @@ -578,6 +583,7 @@ mINI::INIStructure handle_config_ini(bool is_reading, mINI::INIStructure ini) HANDLE_P_VALUE(piano_roll_constrain_edit_to_column) HANDLE_P_VALUE(piano_roll_undo_stack_size) HANDLE_P_VALUE(piano_roll_keep_selection_visible) + HANDLE_P_VALUE(st_undo_load) HANDLE_P_VALUE(use_summercart) HANDLE_P_VALUE(wii_vc_emulation) HANDLE_P_VALUE(is_float_exception_propagation_enabled) diff --git a/shared/Config.hpp b/shared/Config.hpp index 7542fe2e..28b48db9 100644 --- a/shared/Config.hpp +++ b/shared/Config.hpp @@ -45,6 +45,7 @@ enum class Action LoadSlot, SaveAs, LoadAs, + UndoLoadState, SaveSlot1, SaveSlot2, SaveSlot3, @@ -168,6 +169,7 @@ typedef struct Config t_hotkey load_current_hotkey; t_hotkey save_as_hotkey; t_hotkey load_as_hotkey; + t_hotkey undo_load_state_hotkey; t_hotkey save_to_slot_1_hotkey; t_hotkey save_to_slot_2_hotkey; t_hotkey save_to_slot_3_hotkey; @@ -488,6 +490,11 @@ typedef struct Config /// int32_t piano_roll_keep_selection_visible; + /// + /// Whether undo savestate load functionality is enabled. + /// + int32_t st_undo_load = 1; + /// /// SD card emulation /// diff --git a/view/gui/FrontendService.cpp b/view/gui/FrontendService.cpp index e3dc1f1e..87ec84a8 100644 --- a/view/gui/FrontendService.cpp +++ b/view/gui/FrontendService.cpp @@ -200,6 +200,9 @@ void FrontendService::set_default_hotkey_keys(t_config* config) config->load_as_hotkey.key = 'M'; config->load_as_hotkey.ctrl = true; + config->undo_load_state_hotkey.key = 'Z'; + config->undo_load_state_hotkey.ctrl = true; + config->save_to_slot_1_hotkey.key = '1'; config->save_to_slot_1_hotkey.shift = true; diff --git a/view/gui/Main.cpp b/view/gui/Main.cpp index 7dc92137..f6c6b67f 100644 --- a/view/gui/Main.cpp +++ b/view/gui/Main.cpp @@ -135,6 +135,7 @@ const std::map ACTION_ID_MAP = { {Action::LoadSlot, IDM_LOAD_SLOT}, {Action::SaveAs, IDM_SAVE_STATE_AS}, {Action::LoadAs, IDM_LOAD_STATE_AS}, + {Action::UndoLoadState, IDM_UNDO_LOAD_STATE}, {Action::SaveSlot1, (ID_SAVE_1 - 1) + 1}, {Action::SaveSlot2, (ID_SAVE_1 - 1) + 2}, {Action::SaveSlot3, (ID_SAVE_1 - 1) + 3}, @@ -1328,6 +1329,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) EnableMenuItem(g_main_menu, IDM_LOAD_SLOT, emu_launched ? MF_ENABLED : MF_GRAYED); EnableMenuItem(g_main_menu, IDM_SAVE_STATE_AS, emu_launched ? MF_ENABLED : MF_GRAYED); EnableMenuItem(g_main_menu, IDM_LOAD_STATE_AS, emu_launched ? MF_ENABLED : MF_GRAYED); + EnableMenuItem(g_main_menu, IDM_UNDO_LOAD_STATE, (emu_launched && g_config.st_undo_load) ? MF_ENABLED : MF_GRAYED); for (int i = IDM_SELECT_1; i < IDM_SELECT_10; ++i) { EnableMenuItem(g_main_menu, i, emu_launched ? MF_ENABLED : MF_GRAYED); @@ -1771,6 +1773,39 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) }); } break; + case IDM_UNDO_LOAD_STATE: + { + ++g_vr_wait_before_input_poll; + AsyncExecutor::invoke_async([=] + { + --g_vr_wait_before_input_poll; + + auto buf = Savestates::get_undo_savestate(); + + if (buf.empty()) + { + Statusbar::post(L"No load to undo"); + return; + } + + Savestates::do_memory(buf, Savestates::Job::Load, [](const CoreResult result, auto) + { + if (result == CoreResult::Ok) + { + Statusbar::post(L"Undid load"); + return; + } + + if (result == CoreResult::ST_Cancelled) + { + return; + } + + Statusbar::post(L"Failed to undo load"); + }); + }); + } + break; case IDM_START_MOVIE_RECORDING: { BetterEmulationLock lock; diff --git a/view/gui/features/ConfigDialog.cpp b/view/gui/features/ConfigDialog.cpp index a45269ac..dc2e4832 100644 --- a/view/gui/features/ConfigDialog.cpp +++ b/view/gui/features/ConfigDialog.cpp @@ -1038,6 +1038,13 @@ void get_config_listview_items(std::vector& groups, std::vector return emu_launched; }, }, + t_options_item{ + .group_id = core_group.id, + .name = L"Undo Savestate Load", + .tooltip = L"Whether undo savestate load functionality is enabled.", + .data = &g_config.st_undo_load, + .type = t_options_item::Type::Bool, + }, t_options_item{ .group_id = core_group.id, .name = L"Counter Factor", diff --git a/view/resource.h b/view/resource.h index 26d4c5ff..126d189b 100644 --- a/view/resource.h +++ b/view/resource.h @@ -505,6 +505,7 @@ #define IDC_SEEKER_SUBTEXT 40091 #define IDC_VERSION_TEXT 40092 #define IDC_PLUGIN_WARNING 40093 +#define IDM_UNDO_LOAD_STATE 40094 #define IDC_STATIC -1 // Next default values for new objects diff --git a/view/rsrc.rc b/view/rsrc.rc index 34ab72f1..2fde87cc 100644 --- a/view/rsrc.rc +++ b/view/rsrc.rc @@ -67,6 +67,7 @@ BEGIN MENUITEM "Load State", IDM_LOAD_SLOT, GRAYED MENUITEM "&Save State As...", IDM_SAVE_STATE_AS, GRAYED MENUITEM "&Load State As...", IDM_LOAD_STATE_AS, GRAYED + MENUITEM "Undo Load State", IDM_UNDO_LOAD_STATE, GRAYED MENUITEM SEPARATOR POPUP "Current Save S&tate" BEGIN