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