// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "system.h" #include "IconsFontAwesome5.h" #include "achievements.h" #include "bios.h" #include "bus.h" #include "cdrom.h" #include "cheats.h" #include "controller.h" #include "cpu_code_cache.h" #include "cpu_core.h" #include "dma.h" #include "fullscreen_ui.h" #include "game_database.h" #include "game_list.h" #include "gpu.h" #include "gte.h" #include "host.h" #include "host_interface_progress_callback.h" #include "imgui_overlays.h" #include "interrupt_controller.h" #include "mdec.h" #include "memory_card.h" #include "multitap.h" #include "pad.h" #include "pcdrv.h" #include "pgxp.h" #include "psf_loader.h" #include "save_state_version.h" #include "sio.h" #include "spu.h" #include "texture_replacements.h" #include "timers.h" #include "util/audio_stream.h" #include "util/cd_image.h" #include "util/gpu_device.h" #include "util/imgui_manager.h" #include "util/ini_settings_interface.h" #include "util/input_manager.h" #include "util/iso_reader.h" #include "util/platform_misc.h" #include "util/postprocessing.h" #include "util/state_wrapper.h" #include "common/error.h" #include "common/file_system.h" #include "common/log.h" #include "common/path.h" #include "common/string_util.h" #include "common/threading.h" #include "fmt/chrono.h" #include "fmt/format.h" #include "imgui.h" #include "xxhash.h" #include #include #include #include #include #include #include #include Log_SetChannel(System); #ifdef _WIN32 #include "common/windows_headers.h" #include #endif #ifdef ENABLE_DISCORD_PRESENCE #include "discord_rpc.h" #endif // #define PROFILE_MEMORY_SAVE_STATES 1 SystemBootParameters::SystemBootParameters() = default; SystemBootParameters::SystemBootParameters(const SystemBootParameters&) = default; SystemBootParameters::SystemBootParameters(SystemBootParameters&& other) = default; SystemBootParameters::SystemBootParameters(std::string filename_) : filename(std::move(filename_)) { } SystemBootParameters::~SystemBootParameters() = default; namespace System { static std::optional InternalGetExtendedSaveStateInfo(ByteStream* stream); static bool LoadEXE(const char* filename); static std::string GetExecutableNameForImage(IsoReader& iso, bool strip_subdirectories); static bool ReadExecutableFromImage(IsoReader& iso, std::string* out_executable_name, std::vector* out_executable_data); static bool LoadBIOS(const std::string& override_bios_path); static void InternalReset(); static void ClearRunningGame(); static void DestroySystem(); static std::string GetMediaPathFromSaveState(const char* path); static bool DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_display, bool is_memory_state); static bool CreateGPU(GPURenderer renderer, bool is_switching); static bool SaveUndoLoadState(); /// Throttles the system, i.e. sleeps until it's time to execute the next frame. static void Throttle(); static void SetRewinding(bool enabled); static bool SaveRewindState(); static void DoRewind(); static void SaveRunaheadState(); static bool DoRunahead(); static bool Initialize(bool force_software_renderer); static bool FastForwardToFirstFrame(); static bool UpdateGameSettingsLayer(); static void UpdateRunningGame(const char* path, CDImage* image, bool booting); static bool CheckForSBIFile(CDImage* image); static std::unique_ptr GetMemoryCardForSlot(u32 slot, MemoryCardType type); static void UpdateSessionTime(const std::string& prev_serial); static void SetTimerResolutionIncreased(bool enabled); #ifdef ENABLE_DISCORD_PRESENCE static void InitializeDiscordPresence(); static void ShutdownDiscordPresence(); static void PollDiscordPresence(); #endif } // namespace System static constexpr const float PERFORMANCE_COUNTER_UPDATE_INTERVAL = 1.0f; static constexpr const char FALLBACK_EXE_NAME[] = "PSX.EXE"; static std::unique_ptr s_game_settings_interface; static std::unique_ptr s_input_settings_interface; static std::string s_input_profile_name; static System::State s_state = System::State::Shutdown; static std::atomic_bool s_startup_cancelled{false}; static bool s_keep_gpu_device_on_shutdown = false; static ConsoleRegion s_region = ConsoleRegion::NTSC_U; TickCount System::g_ticks_per_second = System::MASTER_CLOCK; static TickCount s_max_slice_ticks = System::MASTER_CLOCK / 10; static u32 s_frame_number = 1; static u32 s_internal_frame_number = 1; static const BIOS::ImageInfo* s_bios_image_info = nullptr; static BIOS::Hash s_bios_hash = {}; static std::string s_running_game_path; static std::string s_running_game_serial; static std::string s_running_game_title; static const GameDatabase::Entry* s_running_game_entry = nullptr; static System::GameHash s_running_game_hash; static bool s_was_fast_booted; static float s_throttle_frequency = 60.0f; static float s_target_speed = 1.0f; static Common::Timer::Value s_frame_period = 0; static Common::Timer::Value s_next_frame_time = 0; static bool s_last_frame_skipped = false; static bool s_system_executing = false; static bool s_system_interrupted = false; static bool s_frame_step_request = false; static bool s_fast_forward_enabled = false; static bool s_turbo_enabled = false; static bool s_throttler_enabled = true; static bool s_display_all_frames = true; static bool s_syncing_to_host = false; static float s_average_frame_time_accumulator = 0.0f; static float s_minimum_frame_time_accumulator = 0.0f; static float s_maximum_frame_time_accumulator = 0.0f; static float s_vps = 0.0f; static float s_fps = 0.0f; static float s_speed = 0.0f; static float s_minimum_frame_time = 0.0f; static float s_maximum_frame_time = 0.0f; static float s_average_frame_time = 0.0f; static float s_cpu_thread_usage = 0.0f; static float s_cpu_thread_time = 0.0f; static float s_sw_thread_usage = 0.0f; static float s_sw_thread_time = 0.0f; static float s_average_gpu_time = 0.0f; static float s_accumulated_gpu_time = 0.0f; static float s_gpu_usage = 0.0f; static System::FrameTimeHistory s_frame_time_history; static u32 s_frame_time_history_pos = 0; static u32 s_last_frame_number = 0; static u32 s_last_internal_frame_number = 0; static u32 s_last_global_tick_counter = 0; static u64 s_last_cpu_time = 0; static u64 s_last_sw_time = 0; static u32 s_presents_since_last_update = 0; static Common::Timer s_fps_timer; static Common::Timer s_frame_timer; static Threading::ThreadHandle s_cpu_thread_handle; static std::unique_ptr s_cheat_list; // temporary save state, created when loading, used to undo load state static std::unique_ptr m_undo_load_state; static bool s_memory_saves_enabled = false; static std::deque s_rewind_states; static s32 s_rewind_load_frequency = -1; static s32 s_rewind_load_counter = -1; static s32 s_rewind_save_frequency = -1; static s32 s_rewind_save_counter = -1; static bool s_rewinding_first_save = false; static std::deque s_runahead_states; static bool s_runahead_replay_pending = false; static u32 s_runahead_frames = 0; static u32 s_runahead_replay_frames = 0; // Used to track play time. We use a monotonic timer here, in case of clock changes. static u64 s_session_start_time = 0; #ifdef ENABLE_DISCORD_PRESENCE static bool s_discord_presence_active = false; #endif static TinyString GetTimestampStringForFileName() { return TinyString::from_fmt("{:%Y-%m-%d_%H-%M-%S}", fmt::localtime(std::time(nullptr))); } void System::Internal::ProcessStartup() { if (!Bus::AllocateMemory()) Panic("Failed to allocate memory for emulated bus."); CPU::CodeCache::ProcessStartup(); // This will call back to Host::LoadSettings() -> ReloadSources(). LoadSettings(false); #ifdef ENABLE_RAINTEGRATION if (Host::GetBaseBoolSettingValue("Cheevos", "UseRAIntegration", false)) Achievements::SwitchToRAIntegration(); #endif if (g_settings.achievements_enabled) Achievements::Initialize(); #ifdef ENABLE_DISCORD_PRESENCE if (g_settings.enable_discord_presence) InitializeDiscordPresence(); #endif } void System::Internal::ProcessShutdown() { #ifdef ENABLE_DISCORD_PRESENCE ShutdownDiscordPresence(); #endif Achievements::Shutdown(false); InputManager::CloseSources(); CPU::CodeCache::ProcessShutdown(); Bus::ReleaseMemory(); } void System::Internal::IdlePollUpdate() { InputManager::PollSources(); #ifdef ENABLE_DISCORD_PRESENCE PollDiscordPresence(); #endif Achievements::IdleUpdate(); } System::State System::GetState() { return s_state; } void System::SetState(State new_state) { if (s_state == new_state) return; Assert(s_state == State::Paused || s_state == State::Running); Assert(new_state == State::Paused || new_state == State::Running); s_state = new_state; } bool System::IsRunning() { return s_state == State::Running; } bool System::IsExecutionInterrupted() { return s_state != State::Running || s_system_interrupted; } bool System::IsPaused() { return s_state == State::Paused; } bool System::IsShutdown() { return s_state == State::Shutdown; } bool System::IsValid() { return s_state == State::Running || s_state == State::Paused; } bool System::IsValidOrInitializing() { return s_state == State::Starting || s_state == State::Running || s_state == State::Paused; } bool System::IsExecuting() { DebugAssert(s_state != State::Shutdown); return s_system_executing; } bool System::IsStartupCancelled() { return s_startup_cancelled.load(); } void System::CancelPendingStartup() { if (s_state == State::Starting) s_startup_cancelled.store(true); } void System::InterruptExecution() { if (s_system_executing) s_system_interrupted = true; } ConsoleRegion System::GetRegion() { return s_region; } DiscRegion System::GetDiscRegion() { return CDROM::GetDiscRegion(); } bool System::IsPALRegion() { return s_region == ConsoleRegion::PAL; } TickCount System::GetMaxSliceTicks() { return s_max_slice_ticks; } void System::UpdateOverclock() { g_ticks_per_second = ScaleTicksToOverclock(MASTER_CLOCK); s_max_slice_ticks = ScaleTicksToOverclock(MASTER_CLOCK / 10); SPU::CPUClockChanged(); CDROM::CPUClockChanged(); g_gpu->CPUClockChanged(); Timers::CPUClocksChanged(); UpdateThrottlePeriod(); } u32 System::GetFrameNumber() { return s_frame_number; } u32 System::GetInternalFrameNumber() { return s_internal_frame_number; } const std::string& System::GetDiscPath() { return s_running_game_path; } const std::string& System::GetGameSerial() { return s_running_game_serial; } const std::string& System::GetGameTitle() { return s_running_game_title; } const GameDatabase::Entry* System::GetGameDatabaseEntry() { return s_running_game_entry; } System::GameHash System::GetGameHash() { return s_running_game_hash; } bool System::IsRunningUnknownGame() { return !s_running_game_entry; } bool System::WasFastBooted() { return s_was_fast_booted; } const BIOS::ImageInfo* System::GetBIOSImageInfo() { return s_bios_image_info; } const BIOS::Hash& System::GetBIOSHash() { return s_bios_hash; } float System::GetFPS() { return s_fps; } float System::GetVPS() { return s_vps; } float System::GetEmulationSpeed() { return s_speed; } float System::GetAverageFrameTime() { return s_average_frame_time; } float System::GetMinimumFrameTime() { return s_minimum_frame_time; } float System::GetMaximumFrameTime() { return s_maximum_frame_time; } float System::GetThrottleFrequency() { return s_throttle_frequency; } float System::GetCPUThreadUsage() { return s_cpu_thread_usage; } float System::GetCPUThreadAverageTime() { return s_cpu_thread_time; } float System::GetSWThreadUsage() { return s_sw_thread_usage; } float System::GetSWThreadAverageTime() { return s_sw_thread_time; } float System::GetGPUUsage() { return s_gpu_usage; } float System::GetGPUAverageTime() { return s_average_gpu_time; } const System::FrameTimeHistory& System::GetFrameTimeHistory() { return s_frame_time_history; } u32 System::GetFrameTimeHistoryPos() { return s_frame_time_history_pos; } bool System::IsExeFileName(const std::string_view& path) { return (StringUtil::EndsWithNoCase(path, ".exe") || StringUtil::EndsWithNoCase(path, ".psexe") || StringUtil::EndsWithNoCase(path, ".ps-exe")); } bool System::IsPsfFileName(const std::string_view& path) { return (StringUtil::EndsWithNoCase(path, ".psf") || StringUtil::EndsWithNoCase(path, ".minipsf")); } bool System::IsLoadableFilename(const std::string_view& path) { static constexpr const std::array extensions = { ".bin", ".cue", ".img", ".iso", ".chd", ".ecm", ".mds", // discs ".exe", ".psexe", ".ps-exe", // exes ".psf", ".minipsf", // psf ".m3u", // playlists ".pbp", }; for (const char* test_extension : extensions) { if (StringUtil::EndsWithNoCase(path, test_extension)) return true; } return false; } bool System::IsSaveStateFilename(const std::string_view& path) { return StringUtil::EndsWithNoCase(path, ".sav"); } ConsoleRegion System::GetConsoleRegionForDiscRegion(DiscRegion region) { switch (region) { case DiscRegion::NTSC_J: return ConsoleRegion::NTSC_J; case DiscRegion::NTSC_U: case DiscRegion::Other: case DiscRegion::NonPS1: default: return ConsoleRegion::NTSC_U; case DiscRegion::PAL: return ConsoleRegion::PAL; } } std::string System::GetGameHashId(GameHash hash) { return fmt::format("HASH-{:X}", hash); } bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash* out_hash) { IsoReader iso; if (!iso.Open(cdi, 1)) { if (out_id) out_id->clear(); if (out_hash) *out_hash = 0; return false; } std::string id; std::string exe_name; std::vector exe_buffer; if (!ReadExecutableFromImage(iso, &exe_name, &exe_buffer)) { if (out_id) out_id->clear(); if (out_hash) *out_hash = 0; return false; } // Always compute the hash. const u32 track_1_length = cdi->GetTrackLength(1); XXH64_state_t* state = XXH64_createState(); XXH64_reset(state, 0x4242D00C); XXH64_update(state, exe_name.c_str(), exe_name.size()); XXH64_update(state, exe_buffer.data(), exe_buffer.size()); XXH64_update(state, &iso.GetPVD(), sizeof(IsoReader::ISOPrimaryVolumeDescriptor)); XXH64_update(state, &track_1_length, sizeof(track_1_length)); const GameHash hash = XXH64_digest(state); XXH64_freeState(state); Log_DevPrintf("Hash for '%s' - %" PRIX64, exe_name.c_str(), hash); if (exe_name != FALLBACK_EXE_NAME) { // Strip off any subdirectories. const std::string::size_type slash = exe_name.rfind('\\'); if (slash != std::string::npos) id = std::string_view(exe_name).substr(slash + 1); else id = exe_name; // SCES_123.45 -> SCES-12345 for (std::string::size_type pos = 0; pos < id.size();) { if (id[pos] == '.') { id.erase(pos, 1); continue; } if (id[pos] == '_') id[pos] = '-'; else id[pos] = static_cast(std::toupper(id[pos])); pos++; } } if (out_id) { if (id.empty()) *out_id = GetGameHashId(hash); else *out_id = std::move(id); } if (out_hash) *out_hash = hash; return true; } std::string System::GetExecutableNameForImage(IsoReader& iso, bool strip_subdirectories) { // Read SYSTEM.CNF std::vector system_cnf_data; if (!iso.ReadFile("SYSTEM.CNF", &system_cnf_data)) return FALLBACK_EXE_NAME; // Parse lines std::vector> lines; std::pair current_line; bool reading_value = false; for (size_t pos = 0; pos < system_cnf_data.size(); pos++) { const char ch = static_cast(system_cnf_data[pos]); if (ch == '\r' || ch == '\n') { if (!current_line.first.empty()) { lines.push_back(std::move(current_line)); current_line = {}; reading_value = false; } } else if (ch == ' ' || (ch >= 0x09 && ch <= 0x0D)) { continue; } else if (ch == '=' && !reading_value) { reading_value = true; } else { if (reading_value) current_line.second.push_back(ch); else current_line.first.push_back(ch); } } if (!current_line.first.empty()) lines.push_back(std::move(current_line)); // Find the BOOT line auto iter = std::find_if(lines.begin(), lines.end(), [](const auto& it) { return StringUtil::Strcasecmp(it.first.c_str(), "boot") == 0; }); if (iter == lines.end()) { // Fallback to PSX.EXE return FALLBACK_EXE_NAME; } std::string code = iter->second; std::string::size_type pos; if (strip_subdirectories) { // cdrom:\SCES_123.45;1 pos = code.rfind('\\'); if (pos != std::string::npos) { code.erase(0, pos + 1); } else { // cdrom:SCES_123.45;1 pos = code.rfind(':'); if (pos != std::string::npos) code.erase(0, pos + 1); } } else { if (code.compare(0, 6, "cdrom:") == 0) code.erase(0, 6); else Log_WarningPrintf("Unknown prefix in executable path: '%s'", code.c_str()); // remove leading slashes while (code[0] == '/' || code[0] == '\\') code.erase(0, 1); } // strip off ; or version number pos = code.rfind(';'); if (pos != std::string::npos) code.erase(pos); return code; } std::string System::GetExecutableNameForImage(CDImage* cdi, bool strip_subdirectories) { IsoReader iso; if (!iso.Open(cdi, 1)) return {}; return GetExecutableNameForImage(iso, strip_subdirectories); } bool System::ReadExecutableFromImage(CDImage* cdi, std::string* out_executable_name, std::vector* out_executable_data) { IsoReader iso; if (!iso.Open(cdi, 1)) return false; return ReadExecutableFromImage(iso, out_executable_name, out_executable_data); } bool System::ReadExecutableFromImage(IsoReader& iso, std::string* out_executable_name, std::vector* out_executable_data) { const std::string executable_path = GetExecutableNameForImage(iso, false); Log_DevPrintf("Executable path: '%s'", executable_path.c_str()); if (!executable_path.empty() && out_executable_data) { if (!iso.ReadFile(executable_path.c_str(), out_executable_data)) { Log_ErrorPrintf("Failed to read executable '%s' from disc", executable_path.c_str()); return false; } } if (out_executable_name) *out_executable_name = std::move(executable_path); return true; } DiscRegion System::GetRegionForSerial(std::string_view serial) { std::string prefix; for (size_t pos = 0; pos < serial.length(); pos++) { const int ch = std::tolower(serial[pos]); if (ch < 'a' || ch > 'z') break; prefix.push_back(static_cast(ch)); } if (prefix == "sces" || prefix == "sced" || prefix == "sles" || prefix == "sled") return DiscRegion::PAL; else if (prefix == "scps" || prefix == "slps" || prefix == "slpm" || prefix == "sczs" || prefix == "papx") return DiscRegion::NTSC_J; else if (prefix == "scus" || prefix == "slus") return DiscRegion::NTSC_U; else return DiscRegion::Other; } DiscRegion System::GetRegionFromSystemArea(CDImage* cdi) { // The license code is on sector 4 of the disc. u8 sector[CDImage::DATA_SECTOR_SIZE]; if (!cdi->Seek(1, 4) || cdi->Read(CDImage::ReadMode::DataOnly, 1, sector) != 1) return DiscRegion::Other; static constexpr char ntsc_u_string[] = " Licensed by Sony Computer Entertainment Amer ica "; static constexpr char ntsc_j_string[] = " Licensed by Sony Computer Entertainment Inc."; static constexpr char pal_string[] = " Licensed by Sony Computer Entertainment Euro pe"; // subtract one for the terminating null if (std::equal(ntsc_u_string, ntsc_u_string + countof(ntsc_u_string) - 1, sector)) return DiscRegion::NTSC_U; else if (std::equal(ntsc_j_string, ntsc_j_string + countof(ntsc_j_string) - 1, sector)) return DiscRegion::NTSC_J; else if (std::equal(pal_string, pal_string + countof(pal_string) - 1, sector)) return DiscRegion::PAL; else return DiscRegion::Other; } DiscRegion System::GetRegionForImage(CDImage* cdi) { const DiscRegion system_area_region = GetRegionFromSystemArea(cdi); if (system_area_region != DiscRegion::Other) return system_area_region; IsoReader iso; if (!iso.Open(cdi, 1)) return DiscRegion::NonPS1; // The executable must exist, because this just returns PSX.EXE if it doesn't. const std::string exename = GetExecutableNameForImage(iso, false); if (exename.empty() || !iso.FileExists(exename.c_str())) return DiscRegion::NonPS1; // Strip off any subdirectories. const std::string::size_type slash = exename.rfind('\\'); if (slash != std::string::npos) return GetRegionForSerial(std::string_view(exename).substr(slash + 1)); else return GetRegionForSerial(exename); } DiscRegion System::GetRegionForExe(const char* path) { auto fp = FileSystem::OpenManagedCFile(path, "rb"); if (!fp) return DiscRegion::Other; BIOS::PSEXEHeader header; if (std::fread(&header, sizeof(header), 1, fp.get()) != 1) return DiscRegion::Other; return BIOS::GetPSExeDiscRegion(header); } DiscRegion System::GetRegionForPsf(const char* path) { PSFLoader::File psf; if (!psf.Load(path)) return DiscRegion::Other; return psf.GetRegion(); } std::optional System::GetRegionForPath(const char* image_path) { if (IsExeFileName(image_path)) return GetRegionForExe(image_path); else if (IsPsfFileName(image_path)) return GetRegionForPsf(image_path); std::unique_ptr cdi = CDImage::Open(image_path, false, nullptr); if (!cdi) return {}; return GetRegionForImage(cdi.get()); } std::string System::GetGameSettingsPath(const std::string_view& game_serial) { const std::string sanitized_serial(Path::SanitizeFileName(game_serial)); return Path::Combine(EmuFolders::GameSettings, fmt::format("{}.ini", sanitized_serial)); } std::string System::GetInputProfilePath(const std::string_view& name) { return Path::Combine(EmuFolders::InputProfiles, fmt::format("{}.ini", name)); } bool System::RecreateGPU(GPURenderer renderer, bool force_recreate_device, bool update_display /* = true*/) { ClearMemorySaveStates(); g_gpu->RestoreDeviceContext(); // save current state std::unique_ptr state_stream = ByteStream::CreateGrowableMemoryStream(); StateWrapper sw(state_stream.get(), StateWrapper::Mode::Write, SAVE_STATE_VERSION); const bool state_valid = g_gpu->DoState(sw, nullptr, false) && TimingEvents::DoState(sw); if (!state_valid) Log_ErrorPrintf("Failed to save old GPU state when switching renderers"); // create new renderer g_gpu.reset(); if (force_recreate_device) Host::ReleaseGPUDevice(); if (!CreateGPU(renderer, true)) { if (!IsStartupCancelled()) Host::ReportErrorAsync("Error", "Failed to recreate GPU."); DestroySystem(); return false; } if (state_valid) { state_stream->SeekAbsolute(0); sw.SetMode(StateWrapper::Mode::Read); g_gpu->RestoreDeviceContext(); g_gpu->DoState(sw, nullptr, update_display); TimingEvents::DoState(sw); } // fix up vsync etc UpdateSpeedLimiterState(); return true; } void System::LoadSettings(bool display_osd_messages) { std::unique_lock lock = Host::GetSettingsLock(); SettingsInterface& si = *Host::GetSettingsInterface(); g_settings.Load(si); g_settings.UpdateLogSettings(); Host::LoadSettings(si, lock); InputManager::ReloadSources(si, lock); InputManager::ReloadBindings(si, *Host::GetSettingsInterfaceForBindings()); // apply compatibility settings if (g_settings.apply_compatibility_settings && !s_running_game_serial.empty()) { const GameDatabase::Entry* entry = GameDatabase::GetEntryForSerial(s_running_game_serial); if (entry) entry->ApplySettings(g_settings, display_osd_messages); } g_settings.FixIncompatibleSettings(display_osd_messages); } void System::SetDefaultSettings(SettingsInterface& si) { Settings temp; // we don't want to reset some things (e.g. OSD) temp.display_show_osd_messages = g_settings.display_show_osd_messages; temp.display_show_fps = g_settings.display_show_fps; temp.display_show_speed = g_settings.display_show_speed; temp.display_show_resolution = g_settings.display_show_resolution; temp.display_show_cpu = g_settings.display_show_cpu; temp.display_show_gpu = g_settings.display_show_gpu; temp.display_show_frame_times = g_settings.display_show_frame_times; // keep controller, we reset it elsewhere for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) temp.controller_types[i] = g_settings.controller_types[i]; temp.Save(si); } void System::ApplySettings(bool display_osd_messages) { Log_DevPrint("Applying settings..."); const Settings old_config(std::move(g_settings)); g_settings = Settings(); LoadSettings(display_osd_messages); // If we've disabled/enabled game settings, we need to reload without it. if (g_settings.apply_game_settings != old_config.apply_game_settings) { UpdateGameSettingsLayer(); LoadSettings(display_osd_messages); } CheckForSettingsChanges(old_config); Host::CheckForSettingsChanges(old_config); if (IsValid()) { ResetPerformanceCounters(); if (s_system_executing) s_system_interrupted = true; } } bool System::ReloadGameSettings(bool display_osd_messages) { if (!IsValid() || !UpdateGameSettingsLayer()) return false; ApplySettings(display_osd_messages); return true; } bool System::UpdateGameSettingsLayer() { std::unique_ptr new_interface; if (g_settings.apply_game_settings && !s_running_game_serial.empty()) { std::string filename(GetGameSettingsPath(s_running_game_serial)); if (FileSystem::FileExists(filename.c_str())) { Log_InfoPrintf("Loading game settings from '%s'...", filename.c_str()); new_interface = std::make_unique(std::move(filename)); if (!new_interface->Load()) { Log_ErrorPrintf("Failed to parse game settings ini '%s'", new_interface->GetFileName().c_str()); new_interface.reset(); } } else { Log_InfoPrintf("No game settings found (tried '%s')", filename.c_str()); } } std::string input_profile_name; bool use_game_settings_for_controller = false; if (new_interface) { new_interface->GetBoolValue("ControllerPorts", "UseGameSettingsForController", &use_game_settings_for_controller); if (!use_game_settings_for_controller) new_interface->GetStringValue("ControllerPorts", "InputProfileName", &input_profile_name); } if (!s_game_settings_interface && !new_interface && s_input_profile_name == input_profile_name) return false; Host::Internal::SetGameSettingsLayer(new_interface.get()); s_game_settings_interface = std::move(new_interface); std::unique_ptr input_interface; if (!use_game_settings_for_controller) { if (!input_profile_name.empty()) { const std::string filename(GetInputProfilePath(input_profile_name)); if (FileSystem::FileExists(filename.c_str())) { Log_InfoPrintf("Loading input profile from '%s'...", filename.c_str()); input_interface = std::make_unique(std::move(filename)); if (!input_interface->Load()) { Log_ErrorPrintf("Failed to parse input profile ini '%s'", input_interface->GetFileName().c_str()); input_interface.reset(); input_profile_name = {}; } } else { Log_InfoPrintf("No input profile found (tried '%s')", filename.c_str()); input_profile_name = {}; } } Host::Internal::SetInputSettingsLayer(input_interface ? input_interface.get() : Host::Internal::GetBaseSettingsLayer()); } else { // using game settings for bindings too Host::Internal::SetInputSettingsLayer(s_game_settings_interface.get()); } s_input_settings_interface = std::move(input_interface); s_input_profile_name = std::move(input_profile_name); return true; } void System::ResetSystem() { if (!IsValid()) return; if (!Achievements::ConfirmSystemReset()) return; if (Achievements::ResetHardcoreMode()) ApplySettings(false); InternalReset(); ResetPerformanceCounters(); ResetThrottler(); Host::AddOSDMessage(TRANSLATE_STR("OSDMessage", "System reset.")); } void System::PauseSystem(bool paused) { if (paused == IsPaused() || !IsValid()) return; SetState(paused ? State::Paused : State::Running); SPU::GetOutputStream()->SetPaused(paused); if (paused) { FullscreenUI::OnSystemPaused(); InputManager::PauseVibration(); Achievements::OnSystemPaused(true); if (g_settings.inhibit_screensaver) PlatformMisc::ResumeScreensaver(); Host::OnSystemPaused(); InvalidateDisplay(); } else { FullscreenUI::OnSystemResumed(); Achievements::OnSystemPaused(false); if (g_settings.inhibit_screensaver) PlatformMisc::SuspendScreensaver(); UpdateDisplaySync(); Host::OnSystemResumed(); ResetPerformanceCounters(); ResetThrottler(); } } bool System::LoadState(const char* filename) { if (!IsValid()) return false; if (Achievements::IsHardcoreModeActive()) { Achievements::ConfirmHardcoreModeDisableAsync(TRANSLATE("Achievements", "Loading state"), [filename = std::string(filename)](bool approved) { if (approved) LoadState(filename.c_str()); }); return false; } Common::Timer load_timer; std::unique_ptr stream = ByteStream::OpenFile(filename, BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED); if (!stream) return false; Log_InfoPrintf("Loading state from '%s'...", filename); { const std::string display_name(FileSystem::GetDisplayNameFromPath(filename)); Host::AddIconOSDMessage( "load_state", ICON_FA_FOLDER_OPEN, fmt::format(TRANSLATE_FS("OSDMessage", "Loading state from '{}'..."), Path::GetFileName(display_name)), 5.0f); } SaveUndoLoadState(); if (!LoadStateFromStream(stream.get(), true)) { Host::ReportFormattedErrorAsync("Load State Error", TRANSLATE("OSDMessage", "Loading state from '%s' failed. Resetting."), filename); if (m_undo_load_state) UndoLoadState(); return false; } ResetPerformanceCounters(); ResetThrottler(); InvalidateDisplay(); Log_VerbosePrintf("Loading state took %.2f msec", load_timer.GetTimeMilliseconds()); return true; } bool System::SaveState(const char* filename, bool backup_existing_save) { if (backup_existing_save && FileSystem::FileExists(filename)) { const std::string backup_filename(Path::ReplaceExtension(filename, "bak")); if (!FileSystem::RenamePath(filename, backup_filename.c_str())) Log_ErrorPrintf("Failed to rename save state backup '%s'", backup_filename.c_str()); } Common::Timer save_timer; std::unique_ptr stream = ByteStream::OpenFile(filename, BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_WRITE | BYTESTREAM_OPEN_TRUNCATE | BYTESTREAM_OPEN_ATOMIC_UPDATE | BYTESTREAM_OPEN_STREAMED); if (!stream) return false; Log_InfoPrintf("Saving state to '%s'...", filename); const u32 screenshot_size = 256; const bool result = SaveStateToStream(stream.get(), screenshot_size, g_settings.compress_save_states ? SAVE_STATE_HEADER::COMPRESSION_TYPE_ZSTD : SAVE_STATE_HEADER::COMPRESSION_TYPE_NONE); if (!result) { Host::ReportFormattedErrorAsync(TRANSLATE("OSDMessage", "Save State"), TRANSLATE("OSDMessage", "Saving state to '%s' failed."), filename); stream->Discard(); } else { const std::string display_name(FileSystem::GetDisplayNameFromPath(filename)); Host::AddIconOSDMessage( "save_state", ICON_FA_SAVE, fmt::format(TRANSLATE_FS("OSDMessage", "State saved to '{}'."), Path::GetFileName(display_name)), 5.0f); stream->Commit(); } Log_VerbosePrintf("Saving state took %.2f msec", save_timer.GetTimeMilliseconds()); return result; } bool System::SaveResumeState() { if (s_running_game_serial.empty()) return false; const std::string path(GetGameSaveStateFileName(s_running_game_serial, -1)); return SaveState(path.c_str(), false); } bool System::BootSystem(SystemBootParameters parameters) { if (!parameters.save_state.empty()) { // loading a state, so pull the media path from the save state to avoid a double change std::string state_media(GetMediaPathFromSaveState(parameters.save_state.c_str())); if (FileSystem::FileExists(state_media.c_str())) parameters.filename = std::move(state_media); } if (parameters.filename.empty()) Log_InfoPrintf("Boot Filename: "); else Log_InfoPrintf("Boot Filename: %s", parameters.filename.c_str()); Assert(s_state == State::Shutdown); s_state = State::Starting; s_startup_cancelled.store(false); s_keep_gpu_device_on_shutdown = static_cast(g_gpu_device); s_region = g_settings.region; Host::OnSystemStarting(); // Load CD image up and detect region. Error error; std::unique_ptr disc; DiscRegion disc_region = DiscRegion::NonPS1; std::string exe_boot; std::string psf_boot; if (!parameters.filename.empty()) { const bool do_exe_boot = IsExeFileName(parameters.filename); const bool do_psf_boot = (!do_exe_boot && IsPsfFileName(parameters.filename)); if (do_exe_boot || do_psf_boot) { if (s_region == ConsoleRegion::Auto) { const DiscRegion file_region = (do_exe_boot ? GetRegionForExe(parameters.filename.c_str()) : GetRegionForPsf(parameters.filename.c_str())); Log_InfoPrintf("EXE/PSF Region: %s", Settings::GetDiscRegionDisplayName(file_region)); s_region = GetConsoleRegionForDiscRegion(file_region); } if (do_psf_boot) psf_boot = std::move(parameters.filename); else exe_boot = std::move(parameters.filename); } else { Log_InfoPrintf("Loading CD image '%s'...", parameters.filename.c_str()); disc = CDImage::Open(parameters.filename.c_str(), g_settings.cdrom_load_image_patches, &error); if (!disc) { Host::ReportErrorAsync("Error", fmt::format("Failed to load CD image '{}': {}", Path::GetFileName(parameters.filename), error.GetDescription())); s_state = State::Shutdown; Host::OnSystemDestroyed(); return false; } disc_region = GetRegionForImage(disc.get()); if (s_region == ConsoleRegion::Auto) { if (disc_region != DiscRegion::Other) { s_region = GetConsoleRegionForDiscRegion(disc_region); Log_InfoPrintf("Auto-detected console %s region for '%s' (region %s)", Settings::GetConsoleRegionName(s_region), parameters.filename.c_str(), Settings::GetDiscRegionName(disc_region)); } else { s_region = ConsoleRegion::NTSC_U; Log_WarningPrintf("Could not determine console region for disc region %s. Defaulting to %s.", Settings::GetDiscRegionName(disc_region), Settings::GetConsoleRegionName(s_region)); } } } } else { // Default to NTSC for BIOS boot. if (s_region == ConsoleRegion::Auto) s_region = ConsoleRegion::NTSC_U; } Log_InfoPrintf("Console Region: %s", Settings::GetConsoleRegionDisplayName(s_region)); // Switch subimage. if (disc && parameters.media_playlist_index != 0 && !disc->SwitchSubImage(parameters.media_playlist_index, &error)) { Host::ReportErrorAsync("Error", fmt::format("Failed to switch to subimage {} in '{}': {}", parameters.media_playlist_index, parameters.filename, error.GetDescription())); s_state = State::Shutdown; Host::OnSystemDestroyed(); return false; } // Update running game, this will apply settings as well. UpdateRunningGame(disc ? disc->GetFileName().c_str() : parameters.filename.c_str(), disc.get(), true); if (!parameters.override_exe.empty()) { if (!FileSystem::FileExists(parameters.override_exe.c_str()) || !IsExeFileName(parameters.override_exe)) { Host::ReportFormattedErrorAsync("Error", "File '%s' is not a valid executable to boot.", parameters.override_exe.c_str()); s_state = State::Shutdown; Host::OnSystemDestroyed(); return false; } Log_InfoPrintf("Overriding boot executable: '%s'", parameters.override_exe.c_str()); exe_boot = std::move(parameters.override_exe); } // Check for SBI. if (!CheckForSBIFile(disc.get())) { s_state = State::Shutdown; ClearRunningGame(); Host::OnSystemDestroyed(); return false; } // Check for resuming with hardcore mode. if (parameters.disable_achievements_hardcore_mode) Achievements::DisableHardcoreMode(); if (!parameters.save_state.empty() && Achievements::IsHardcoreModeActive()) { bool cancelled; if (FullscreenUI::IsInitialized()) { Achievements::ConfirmHardcoreModeDisableAsync(TRANSLATE("Achievements", "Resuming state"), [parameters = std::move(parameters)](bool approved) mutable { if (approved) { parameters.disable_achievements_hardcore_mode = true; BootSystem(std::move(parameters)); } }); cancelled = true; } else { cancelled = !Achievements::ConfirmHardcoreModeDisable(TRANSLATE("Achievements", "Resuming state")); } if (cancelled) { s_state = State::Shutdown; ClearRunningGame(); Host::OnSystemDestroyed(); return false; } } // Load BIOS image. if (!LoadBIOS(parameters.override_bios)) { s_state = State::Shutdown; ClearRunningGame(); Host::OnSystemDestroyed(); return false; } // Component setup. if (!Initialize(parameters.force_software_renderer)) { s_state = State::Shutdown; ClearRunningGame(); Host::OnSystemDestroyed(); return false; } // Insert disc. if (disc) CDROM::InsertMedia(std::move(disc), disc_region); UpdateControllers(); UpdateMemoryCardTypes(); UpdateMultitaps(); InternalReset(); // Load EXE late after BIOS. if (!exe_boot.empty() && !LoadEXE(exe_boot.c_str())) { Host::ReportFormattedErrorAsync("Error", "Failed to load EXE file '%s'", exe_boot.c_str()); DestroySystem(); return false; } else if (!psf_boot.empty() && !PSFLoader::Load(psf_boot.c_str())) { Host::ReportFormattedErrorAsync("Error", "Failed to load PSF file '%s'", psf_boot.c_str()); DestroySystem(); return false; } // Apply fastboot patch if enabled. if (CDROM::HasMedia() && (parameters.override_fast_boot.has_value() ? parameters.override_fast_boot.value() : g_settings.bios_patch_fast_boot)) { if (s_bios_image_info && s_bios_image_info->patch_compatible) { // TODO: Fast boot without patches... BIOS::PatchBIOSFastBoot(Bus::g_bios, Bus::BIOS_SIZE); s_was_fast_booted = true; } else { Log_ErrorPrintf("Not patching fast boot, as BIOS is not patch compatible."); } } // Good to go. s_state = State::Running; SPU::GetOutputStream()->SetPaused(false); FullscreenUI::OnSystemStarted(); if (g_settings.inhibit_screensaver) PlatformMisc::SuspendScreensaver(); Host::OnSystemStarted(); // try to load the state, if it fails, bail out if (!parameters.save_state.empty()) { std::unique_ptr stream = ByteStream::OpenFile(parameters.save_state.c_str(), BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED); if (!stream) { Host::ReportErrorAsync( TRANSLATE("System", "Error"), fmt::format(TRANSLATE_FS("System", "Failed to load save state file '{}' for booting."), parameters.save_state)); DestroySystem(); return false; } if (!LoadStateFromStream(stream.get(), true)) { DestroySystem(); return false; } } if (parameters.load_image_to_ram || g_settings.cdrom_load_image_to_ram) CDROM::PrecacheMedia(); if (parameters.fast_forward_to_first_frame) FastForwardToFirstFrame(); if (g_settings.audio_dump_on_boot) StartDumpingAudio(); if (g_settings.start_paused || parameters.override_start_paused.value_or(false)) PauseSystem(true); ResetPerformanceCounters(); if (IsRunning()) UpdateSpeedLimiterState(); return true; } bool System::Initialize(bool force_software_renderer) { g_ticks_per_second = ScaleTicksToOverclock(MASTER_CLOCK); s_max_slice_ticks = ScaleTicksToOverclock(MASTER_CLOCK / 10); s_frame_number = 1; s_internal_frame_number = 1; s_target_speed = g_settings.emulation_speed; s_throttle_frequency = 60.0f; s_frame_period = 0; s_next_frame_time = 0; s_turbo_enabled = false; s_fast_forward_enabled = false; s_rewind_load_frequency = -1; s_rewind_load_counter = -1; s_rewinding_first_save = true; s_average_frame_time_accumulator = 0.0f; s_minimum_frame_time_accumulator = 0.0f; s_maximum_frame_time_accumulator = 0.0f; s_vps = 0.0f; s_fps = 0.0f; s_speed = 0.0f; s_minimum_frame_time = 0.0f; s_maximum_frame_time = 0.0f; s_average_frame_time = 0.0f; s_cpu_thread_usage = 0.0f; s_cpu_thread_time = 0.0f; s_sw_thread_usage = 0.0f; s_sw_thread_time = 0.0f; s_average_gpu_time = 0.0f; s_accumulated_gpu_time = 0.0f; s_gpu_usage = 0.0f; s_last_frame_number = 0; s_last_internal_frame_number = 0; s_last_global_tick_counter = 0; s_presents_since_last_update = 0; s_last_cpu_time = 0; s_fps_timer.Reset(); s_frame_timer.Reset(); s_frame_time_history.fill(0.0f); s_frame_time_history_pos = 0; TimingEvents::Initialize(); CPU::Initialize(); if (!Bus::Initialize()) { CPU::Shutdown(); return false; } CPU::CodeCache::Initialize(); if (!CreateGPU(force_software_renderer ? GPURenderer::Software : g_settings.gpu_renderer, false)) { Bus::Shutdown(); CPU::Shutdown(); return false; } GTE::UpdateAspectRatio(); if (g_settings.gpu_pgxp_enable) PGXP::Initialize(); // Was startup cancelled? (e.g. shading compilers took too long and the user closed the application) if (IsStartupCancelled()) { g_gpu.reset(); if (!s_keep_gpu_device_on_shutdown) { Host::ReleaseGPUDevice(); Host::ReleaseRenderWindow(); } if (g_settings.gpu_pgxp_enable) PGXP::Shutdown(); CPU::Shutdown(); Bus::Shutdown(); return false; } DMA::Initialize(); InterruptController::Initialize(); CDROM::Initialize(); Pad::Initialize(); Timers::Initialize(); SPU::Initialize(); MDEC::Initialize(); SIO::Initialize(); PCDrv::Initialize(); PostProcessing::Initialize(); static constexpr float WARNING_DURATION = 15.0f; if (g_settings.cpu_overclock_active) { Host::AddFormattedOSDMessage( WARNING_DURATION, TRANSLATE("OSDMessage", "CPU clock speed is set to %u%% (%u / %u). This may result in instability."), g_settings.GetCPUOverclockPercent(), g_settings.cpu_overclock_numerator, g_settings.cpu_overclock_denominator); } if (g_settings.cdrom_read_speedup > 1) { Host::AddFormattedOSDMessage( WARNING_DURATION, TRANSLATE("OSDMessage", "CD-ROM read speedup set to %ux (effective speed %ux). This may result in instability."), g_settings.cdrom_read_speedup, g_settings.cdrom_read_speedup * 2); } if (g_settings.cdrom_seek_speedup != 1) { if (g_settings.cdrom_seek_speedup == 0) { Host::AddOSDMessage( TRANSLATE_STR("OSDMessage", "CD-ROM seek speedup set to instant. This may result in instability."), WARNING_DURATION); } else { Host::AddFormattedOSDMessage( WARNING_DURATION, TRANSLATE("OSDMessage", "CD-ROM seek speedup set to %ux. This may result in instability."), g_settings.cdrom_seek_speedup); } } s_cpu_thread_handle = Threading::ThreadHandle::GetForCallingThread(); UpdateThrottlePeriod(); UpdateMemorySaveStateSettings(); return true; } void System::DestroySystem() { DebugAssert(!s_system_executing); if (s_state == State::Shutdown) return; Host::ClearOSDMessages(); PostProcessing::Shutdown(); SaveStateSelectorUI::Close(true); FullscreenUI::OnSystemDestroyed(); InputManager::PauseVibration(); if (g_settings.inhibit_screensaver) PlatformMisc::ResumeScreensaver(); SetTimerResolutionIncreased(false); s_cpu_thread_usage = {}; ClearMemorySaveStates(); g_texture_replacements.Shutdown(); PCDrv::Shutdown(); SIO::Shutdown(); MDEC::Shutdown(); SPU::Shutdown(); Timers::Shutdown(); Pad::Shutdown(); CDROM::Shutdown(); g_gpu.reset(); InterruptController::Shutdown(); DMA::Shutdown(); PGXP::Shutdown(); CPU::CodeCache::Shutdown(); Bus::Shutdown(); CPU::Shutdown(); TimingEvents::Shutdown(); ClearRunningGame(); // Restore present-all-frames behavior. if (s_keep_gpu_device_on_shutdown && g_gpu_device) { g_gpu_device->SetDisplayMaxFPS(0.0f); } else { Host::ReleaseGPUDevice(); Host::ReleaseRenderWindow(); } s_bios_hash = {}; s_bios_image_info = nullptr; s_was_fast_booted = false; s_cheat_list.reset(); s_state = State::Shutdown; Host::OnSystemDestroyed(); } void System::ClearRunningGame() { UpdateSessionTime(s_running_game_serial); s_running_game_serial.clear(); s_running_game_path.clear(); s_running_game_title.clear(); s_running_game_entry = nullptr; s_running_game_hash = 0; Host::OnGameChanged(s_running_game_path, s_running_game_serial, s_running_game_title); Achievements::GameChanged(s_running_game_path, nullptr); #ifdef ENABLE_DISCORD_PRESENCE UpdateDiscordPresence(); #endif } bool System::FastForwardToFirstFrame() { // If we're taking more than 60 seconds to load the game, oof.. static constexpr u32 MAX_FRAMES_TO_SKIP = 30 * 60; const u32 current_frame_number = s_frame_number; const u32 current_internal_frame_number = s_internal_frame_number; SPU::SetAudioOutputMuted(true); while (s_internal_frame_number == current_internal_frame_number && (s_frame_number - current_frame_number) <= MAX_FRAMES_TO_SKIP) { Panic("Fixme"); // System::RunFrame(); } SPU::SetAudioOutputMuted(false); return (s_internal_frame_number != current_internal_frame_number); } void System::Execute() { for (;;) { switch (s_state) { case State::Running: { s_system_executing = true; // TODO: Purge reset/restore g_gpu->RestoreDeviceContext(); TimingEvents::UpdateCPUDowncount(); if (s_rewind_load_counter >= 0) DoRewind(); else CPU::Execute(); s_system_executing = false; continue; } case State::Stopping: { DestroySystem(); return; } case State::Paused: default: return; } } } void System::FrameDone() { s_frame_number++; // Vertex buffer is shared, need to flush what we have. g_gpu->FlushRender(); // Generate any pending samples from the SPU before sleeping, this way we reduce the chances of underruns. // TODO: when running ahead, we can skip this (and the flush above) SPU::GeneratePendingSamples(); if (s_cheat_list) s_cheat_list->Apply(); if (Achievements::IsActive()) Achievements::FrameUpdate(); #ifdef ENABLE_DISCORD_PRESENCE PollDiscordPresence(); #endif if (s_frame_step_request) { s_frame_step_request = false; PauseSystem(true); } // Save states for rewind and runahead. if (s_rewind_save_counter >= 0) { if (s_rewind_save_counter == 0) { SaveRewindState(); s_rewind_save_counter = s_rewind_save_frequency; } else { s_rewind_save_counter--; } } else if (s_runahead_frames > 0) { // We don't want to poll during replay, because otherwise we'll lose frames. if (s_runahead_replay_frames == 0) { // For runahead, poll input early, that way we can use the remainder of this frame to replay. // *technically* this means higher input latency (by less than a frame), but runahead itself // counter-acts that. Host::PumpMessagesOnCPUThread(); InputManager::PollSources(); g_gpu->RestoreDeviceContext(); if (IsExecutionInterrupted()) { s_system_interrupted = false; CPU::ExitExecution(); return; } } if (DoRunahead()) { // running ahead, get it done as soon as possible return; } SaveRunaheadState(); } const Common::Timer::Value current_time = Common::Timer::GetCurrentValue(); if (current_time < s_next_frame_time || s_display_all_frames || s_last_frame_skipped) { s_last_frame_skipped = !PresentDisplay(true); } else if (current_time >= s_next_frame_time) { Log_DebugPrintf("Skipping displaying frame"); s_last_frame_skipped = true; } if (s_throttler_enabled && !IsExecutionInterrupted()) Throttle(); // Input poll already done above if (s_runahead_frames == 0) { Host::PumpMessagesOnCPUThread(); InputManager::PollSources(); if (IsExecutionInterrupted()) { s_system_interrupted = false; CPU::ExitExecution(); return; } } g_gpu->RestoreDeviceContext(); // Update perf counters *after* throttling, we want to measure from start-of-frame // to start-of-frame, not end-of-frame to end-of-frame (will be noisy due to different // amounts of computation happening in each frame). System::UpdatePerformanceCounters(); } void System::SetThrottleFrequency(float frequency) { if (s_throttle_frequency == frequency) return; s_throttle_frequency = frequency; UpdateThrottlePeriod(); } void System::UpdateThrottlePeriod() { if (s_target_speed > std::numeric_limits::epsilon()) { const double target_speed = std::max(static_cast(s_target_speed), std::numeric_limits::epsilon()); s_frame_period = Common::Timer::ConvertSecondsToValue(1.0 / (static_cast(s_throttle_frequency) * target_speed)); } else { s_frame_period = 1; } ResetThrottler(); } void System::ResetThrottler() { s_next_frame_time = Common::Timer::GetCurrentValue() + s_frame_period; } void System::Throttle() { // If we're running too slow, advance the next frame time based on the time we lost. Effectively skips // running those frames at the intended time, because otherwise if we pause in the debugger, we'll run // hundreds of frames when we resume. Common::Timer::Value current_time = Common::Timer::GetCurrentValue(); if (current_time > s_next_frame_time) { const Common::Timer::Value diff = static_cast(current_time) - static_cast(s_next_frame_time); s_next_frame_time += (diff / s_frame_period) * s_frame_period + s_frame_period; return; } // Use a spinwait if we undersleep for all platforms except android.. don't want to burn battery. // Linux also seems to do a much better job of waking up at the requested time. #if !defined(__linux__) && !defined(__ANDROID__) Common::Timer::SleepUntil(s_next_frame_time, g_settings.display_all_frames); #else Common::Timer::SleepUntil(s_next_frame_time, false); #endif #if 0 Log_DevPrintf("Asked for %.2f ms, slept for %.2f ms, %.2f ms late", Common::Timer::ConvertValueToMilliseconds(s_next_frame_time - current_time), Common::Timer::ConvertValueToMilliseconds(Common::Timer::GetCurrentValue() - current_time), Common::Timer::ConvertValueToMilliseconds(Common::Timer::GetCurrentValue() - s_next_frame_time)); #endif s_next_frame_time += s_frame_period; } void System::SingleStepCPU() { s_frame_timer.Reset(); s_system_executing = true; g_gpu->RestoreDeviceContext(); CPU::SingleStep(); g_gpu->FlushRender(); SPU::GeneratePendingSamples(); InvalidateDisplay(); s_system_executing = false; } void System::IncrementInternalFrameNumber() { s_internal_frame_number++; } void System::RecreateSystem() { Assert(!IsShutdown()); const bool was_paused = System::IsPaused(); std::unique_ptr stream = ByteStream::CreateGrowableMemoryStream(nullptr, 8 * 1024); if (!System::SaveStateToStream(stream.get(), 0, SAVE_STATE_HEADER::COMPRESSION_TYPE_NONE) || !stream->SeekAbsolute(0)) { Host::ReportErrorAsync("Error", "Failed to save state before system recreation. Shutting down."); DestroySystem(); return; } DestroySystem(); SystemBootParameters boot_params; if (!BootSystem(std::move(boot_params))) { Host::ReportErrorAsync("Error", "Failed to boot system after recreation."); return; } if (!LoadStateFromStream(stream.get(), false)) { DestroySystem(); return; } ResetPerformanceCounters(); ResetThrottler(); InvalidateDisplay(); if (was_paused) PauseSystem(true); } bool System::CreateGPU(GPURenderer renderer, bool is_switching) { const RenderAPI api = Settings::GetRenderAPIForRenderer(renderer); if (!g_gpu_device || (renderer != GPURenderer::Software && !GPUDevice::IsSameRenderAPI(g_gpu_device->GetRenderAPI(), api))) { if (g_gpu_device) { Log_WarningPrintf("Recreating GPU device, expecting %s got %s", GPUDevice::RenderAPIToString(api), GPUDevice::RenderAPIToString(g_gpu_device->GetRenderAPI())); PostProcessing::Shutdown(); } Host::ReleaseGPUDevice(); if (!Host::CreateGPUDevice(api)) { Host::ReleaseRenderWindow(); return false; } if (is_switching) PostProcessing::Initialize(); } if (renderer == GPURenderer::Software) g_gpu = GPU::CreateSoftwareRenderer(); else g_gpu = GPU::CreateHardwareRenderer(); if (!g_gpu) { Log_ErrorPrintf("Failed to initialize %s renderer, falling back to software renderer", Settings::GetRendererName(renderer)); Host::AddFormattedOSDMessage( 30.0f, TRANSLATE("OSDMessage", "Failed to initialize %s renderer, falling back to software renderer."), Settings::GetRendererName(renderer)); g_gpu.reset(); g_gpu = GPU::CreateSoftwareRenderer(); if (!g_gpu) { Log_ErrorPrintf("Failed to create fallback software renderer."); if (!s_keep_gpu_device_on_shutdown) { Host::ReleaseGPUDevice(); Host::ReleaseRenderWindow(); } return false; } } return true; } bool System::DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_display, bool is_memory_state) { if (!sw.DoMarker("System")) return false; sw.Do(&s_region); sw.Do(&s_frame_number); sw.Do(&s_internal_frame_number); // Don't bother checking this at all for memory states, since they won't have a different BIOS... if (!is_memory_state) { BIOS::Hash bios_hash = s_bios_hash; sw.DoBytesEx(bios_hash.bytes, sizeof(bios_hash.bytes), 58, s_bios_hash.bytes); if (bios_hash != s_bios_hash) { Log_WarningPrintf("BIOS hash mismatch: System: %s | State: %s", s_bios_hash.ToString().c_str(), bios_hash.ToString().c_str()); Host::AddKeyedOSDMessage("StateBIOSMismatch", TRANSLATE_STR("OSDMessage", "This save state was created with a different BIOS version or patch " "options. This may cause stability issues."), 10.0f); } } if (!sw.DoMarker("CPU") || !CPU::DoState(sw)) return false; if (sw.IsReading()) { if (is_memory_state) CPU::CodeCache::InvalidateAllRAMBlocks(); else CPU::CodeCache::Reset(); } // only reset pgxp if we're not runahead-rollbacking. the value checks will save us from broken rendering, and it // saves using imprecise values for a frame in 30fps games. if (sw.IsReading() && g_settings.gpu_pgxp_enable && !is_memory_state) PGXP::Reset(); if (!sw.DoMarker("Bus") || !Bus::DoState(sw)) return false; if (!sw.DoMarker("DMA") || !DMA::DoState(sw)) return false; if (!sw.DoMarker("InterruptController") || !InterruptController::DoState(sw)) return false; g_gpu->RestoreDeviceContext(); if (!sw.DoMarker("GPU") || !g_gpu->DoState(sw, host_texture, update_display)) return false; if (!sw.DoMarker("CDROM") || !CDROM::DoState(sw)) return false; if (!sw.DoMarker("Pad") || !Pad::DoState(sw, is_memory_state)) return false; if (!sw.DoMarker("Timers") || !Timers::DoState(sw)) return false; if (!sw.DoMarker("SPU") || !SPU::DoState(sw)) return false; if (!sw.DoMarker("MDEC") || !MDEC::DoState(sw)) return false; if (!sw.DoMarker("SIO") || !SIO::DoState(sw)) return false; if (!sw.DoMarker("Events") || !TimingEvents::DoState(sw)) return false; if (!sw.DoMarker("Overclock")) return false; bool cpu_overclock_active = g_settings.cpu_overclock_active; u32 cpu_overclock_numerator = g_settings.cpu_overclock_numerator; u32 cpu_overclock_denominator = g_settings.cpu_overclock_denominator; sw.Do(&cpu_overclock_active); sw.Do(&cpu_overclock_numerator); sw.Do(&cpu_overclock_denominator); if (sw.IsReading() && (cpu_overclock_active != g_settings.cpu_overclock_active || (cpu_overclock_active && (g_settings.cpu_overclock_numerator != cpu_overclock_numerator || g_settings.cpu_overclock_denominator != cpu_overclock_denominator)))) { Host::AddFormattedOSDMessage( 10.0f, TRANSLATE("OSDMessage", "WARNING: CPU overclock (%u%%) was different in save state (%u%%)."), g_settings.cpu_overclock_enable ? g_settings.GetCPUOverclockPercent() : 100u, cpu_overclock_active ? Settings::CPUOverclockFractionToPercent(cpu_overclock_numerator, cpu_overclock_denominator) : 100u); UpdateOverclock(); } if (!is_memory_state) { if (sw.GetVersion() >= 56) { if (!sw.DoMarker("Cheevos")) return false; if (!Achievements::DoState(sw)) return false; } else { // loading an old state without cheevos, so reset the runtime Achievements::ResetClient(); } } return !sw.HasError(); } bool System::LoadBIOS(const std::string& override_bios_path) { std::optional bios_image( override_bios_path.empty() ? BIOS::GetBIOSImage(s_region) : FileSystem::ReadBinaryFile(override_bios_path.c_str())); if (!bios_image.has_value()) { Host::ReportFormattedErrorAsync("Error", TRANSLATE("System", "Failed to load %s BIOS."), Settings::GetConsoleRegionName(s_region)); return false; } if (bios_image->size() != static_cast(Bus::BIOS_SIZE)) { Host::ReportFormattedErrorAsync("Error", TRANSLATE("System", "Incorrect BIOS image size")); return false; } s_bios_hash = BIOS::GetImageHash(bios_image.value()); s_bios_image_info = BIOS::GetInfoForImage(bios_image.value(), s_bios_hash); if (s_bios_image_info) Log_InfoPrintf("Using BIOS: %s", s_bios_image_info->description); else Log_WarningPrintf("Using an unknown BIOS: %s", s_bios_hash.ToString().c_str()); std::memcpy(Bus::g_bios, bios_image->data(), Bus::BIOS_SIZE); return true; } void System::InternalReset() { if (IsShutdown()) return; CPU::Reset(); CPU::CodeCache::Reset(); if (g_settings.gpu_pgxp_enable) PGXP::Initialize(); Bus::Reset(); DMA::Reset(); InterruptController::Reset(); g_gpu->Reset(true); CDROM::Reset(); Pad::Reset(); Timers::Reset(); SPU::Reset(); MDEC::Reset(); SIO::Reset(); PCDrv::Reset(); s_frame_number = 1; s_internal_frame_number = 0; TimingEvents::Reset(); ResetPerformanceCounters(); Achievements::ResetClient(); } std::string System::GetMediaPathFromSaveState(const char* path) { std::string ret; std::unique_ptr stream(ByteStream::OpenFile(path, BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_SEEKABLE)); if (stream) { SAVE_STATE_HEADER header; if (stream->Read2(&header, sizeof(header)) && header.magic == SAVE_STATE_MAGIC && header.version >= SAVE_STATE_MINIMUM_VERSION && header.version <= SAVE_STATE_VERSION) { if (header.media_filename_length > 0) { ret.resize(header.media_filename_length); if (!stream->SeekAbsolute(header.offset_to_media_filename) || !stream->Read2(ret.data(), header.media_filename_length)) { ret = {}; } } } } return ret; } bool System::LoadStateFromStream(ByteStream* state, bool update_display, bool ignore_media) { Assert(IsValid()); SAVE_STATE_HEADER header; if (!state->Read2(&header, sizeof(header))) return false; if (header.magic != SAVE_STATE_MAGIC) return false; if (header.version < SAVE_STATE_MINIMUM_VERSION) { Host::ReportFormattedErrorAsync( "Error", TRANSLATE("System", "Save state is incompatible: minimum version is %u but state is version %u."), SAVE_STATE_MINIMUM_VERSION, header.version); return false; } if (header.version > SAVE_STATE_VERSION) { Host::ReportFormattedErrorAsync( "Error", TRANSLATE("System", "Save state is incompatible: maximum version is %u but state is version %u."), SAVE_STATE_VERSION, header.version); return false; } if (!ignore_media) { Error error; std::string media_filename; std::unique_ptr media; if (header.media_filename_length > 0) { media_filename.resize(header.media_filename_length); if (!state->SeekAbsolute(header.offset_to_media_filename) || !state->Read2(media_filename.data(), header.media_filename_length)) { return false; } std::unique_ptr old_media = CDROM::RemoveMedia(false); if (old_media && old_media->GetFileName() == media_filename) { Log_InfoPrintf("Re-using same media '%s'", media_filename.c_str()); media = std::move(old_media); } else { media = CDImage::Open(media_filename.c_str(), g_settings.cdrom_load_image_patches, &error); if (!media) { if (old_media) { Host::AddOSDMessage( fmt::format(TRANSLATE_FS("OSDMessage", "Failed to open CD image from save state '{}': {}.\nUsing " "existing image '{}', this may result in instability."), media_filename, error.GetDescription(), old_media->GetFileName()), Host::OSD_CRITICAL_ERROR_DURATION); media = std::move(old_media); header.media_subimage_index = media->GetCurrentSubImage(); } else { Host::ReportErrorAsync( TRANSLATE_SV("OSDMessage", "Error"), fmt::format(TRANSLATE_FS("System", "Failed to open CD image '{}' used by save state: {}."), media_filename, error.GetDescription())); return false; } } } } UpdateRunningGame(media_filename.c_str(), media.get(), false); if (media && header.version >= 51) { const u32 num_subimages = media->HasSubImages() ? media->GetSubImageCount() : 1; if (header.media_subimage_index >= num_subimages || (media->HasSubImages() && media->GetCurrentSubImage() != header.media_subimage_index && !media->SwitchSubImage(header.media_subimage_index, &error))) { Host::ReportErrorAsync( TRANSLATE_SV("OSDMessage", "Error"), fmt::format( TRANSLATE_FS("System", "Failed to switch to subimage {} in CD image '{}' used by save state: {}."), header.media_subimage_index + 1u, media_filename, error.GetDescription())); return false; } else { Log_InfoPrintf("Switched to subimage %u in '%s'", header.media_subimage_index, media_filename.c_str()); } } CDROM::Reset(); if (media) { const DiscRegion region = GetRegionForImage(media.get()); CDROM::InsertMedia(std::move(media), region); if (g_settings.cdrom_load_image_to_ram) CDROM::PrecacheMedia(); } else { CDROM::RemoveMedia(false); } // ensure the correct card is loaded if (g_settings.HasAnyPerGameMemoryCards()) UpdatePerGameMemoryCards(); } ClearMemorySaveStates(); // Updating game/loading settings can turn on hardcore mode. Catch this. Achievements::DisableHardcoreMode(); if (!state->SeekAbsolute(header.offset_to_data)) return false; if (header.data_compression_type == SAVE_STATE_HEADER::COMPRESSION_TYPE_NONE) { StateWrapper sw(state, StateWrapper::Mode::Read, header.version); if (!DoState(sw, nullptr, update_display, false)) return false; } else if (header.data_compression_type == SAVE_STATE_HEADER::COMPRESSION_TYPE_ZSTD) { std::unique_ptr dstream(ByteStream::CreateZstdDecompressStream(state, header.data_compressed_size)); StateWrapper sw(dstream.get(), StateWrapper::Mode::Read, header.version); if (!DoState(sw, nullptr, update_display, false)) return false; } else { Host::ReportFormattedErrorAsync("Error", "Unknown save state compression type %u", header.data_compression_type); return false; } if (s_state == State::Starting) s_state = State::Running; ResetPerformanceCounters(); ResetThrottler(); return true; } bool System::SaveStateToStream(ByteStream* state, u32 screenshot_size /* = 256 */, u32 compression_method /* = SAVE_STATE_HEADER::COMPRESSION_TYPE_NONE*/, bool ignore_media /* = false*/) { if (IsShutdown()) return false; SAVE_STATE_HEADER header = {}; const u64 header_position = state->GetPosition(); if (!state->Write2(&header, sizeof(header))) return false; // fill in header header.magic = SAVE_STATE_MAGIC; header.version = SAVE_STATE_VERSION; StringUtil::Strlcpy(header.title, s_running_game_title.c_str(), sizeof(header.title)); StringUtil::Strlcpy(header.serial, s_running_game_serial.c_str(), sizeof(header.serial)); if (CDROM::HasMedia() && !ignore_media) { const std::string& media_filename = CDROM::GetMediaFileName(); header.offset_to_media_filename = static_cast(state->GetPosition()); header.media_filename_length = static_cast(media_filename.length()); header.media_subimage_index = CDROM::GetMedia()->HasSubImages() ? CDROM::GetMedia()->GetCurrentSubImage() : 0; if (!media_filename.empty() && !state->Write2(media_filename.data(), header.media_filename_length)) return false; } // save screenshot if (screenshot_size > 0) { // assume this size is the width const float display_aspect_ratio = g_gpu->GetDisplayAspectRatio(); const u32 screenshot_width = screenshot_size; const u32 screenshot_height = std::max(1u, static_cast(static_cast(screenshot_width) / ((display_aspect_ratio > 0.0f) ? display_aspect_ratio : 1.0f))); Log_VerbosePrintf("Saving %ux%u screenshot for state", screenshot_width, screenshot_height); std::vector screenshot_buffer; u32 screenshot_stride; GPUTexture::Format screenshot_format; if (g_gpu->RenderScreenshotToBuffer(screenshot_width, screenshot_height, Common::Rectangle::FromExtents(0, 0, screenshot_width, screenshot_height), false, &screenshot_buffer, &screenshot_stride, &screenshot_format) && GPUTexture::ConvertTextureDataToRGBA8(screenshot_width, screenshot_height, screenshot_buffer, screenshot_stride, screenshot_format)) { if (screenshot_stride != (screenshot_width * sizeof(u32))) { Log_WarningPrintf("Failed to save %ux%u screenshot for save state due to incorrect stride(%u)", screenshot_width, screenshot_height, screenshot_stride); } else { if (g_gpu_device->UsesLowerLeftOrigin()) { GPUTexture::FlipTextureDataRGBA8(screenshot_width, screenshot_height, screenshot_buffer, screenshot_stride); } header.offset_to_screenshot = static_cast(state->GetPosition()); header.screenshot_width = screenshot_width; header.screenshot_height = screenshot_height; header.screenshot_size = static_cast(screenshot_buffer.size() * sizeof(u32)); if (!state->Write2(screenshot_buffer.data(), header.screenshot_size)) return false; } } else { Log_WarningPrintf("Failed to save %ux%u screenshot for save state due to render/conversion failure", screenshot_width, screenshot_height); } } // write data { header.offset_to_data = static_cast(state->GetPosition()); g_gpu->RestoreDeviceContext(); header.data_compression_type = compression_method; bool result = false; if (compression_method == SAVE_STATE_HEADER::COMPRESSION_TYPE_NONE) { StateWrapper sw(state, StateWrapper::Mode::Write, SAVE_STATE_VERSION); result = DoState(sw, nullptr, false, false); header.data_uncompressed_size = static_cast(state->GetPosition() - header.offset_to_data); } else if (compression_method == SAVE_STATE_HEADER::COMPRESSION_TYPE_ZSTD) { std::unique_ptr cstream(ByteStream::CreateZstdCompressStream(state, 0)); StateWrapper sw(cstream.get(), StateWrapper::Mode::Write, SAVE_STATE_VERSION); result = DoState(sw, nullptr, false, false) && cstream->Commit(); header.data_uncompressed_size = static_cast(cstream->GetPosition()); header.data_compressed_size = static_cast(state->GetPosition() - header.offset_to_data); } if (!result) return false; } // re-write header const u64 end_position = state->GetPosition(); if (!state->SeekAbsolute(header_position) || !state->Write2(&header, sizeof(header)) || !state->SeekAbsolute(end_position)) { return false; } return true; } float System::GetTargetSpeed() { return s_target_speed; } void System::UpdatePerformanceCounters() { const float frame_time = static_cast(s_frame_timer.GetTimeMillisecondsAndReset()); s_minimum_frame_time_accumulator = (s_minimum_frame_time_accumulator == 0.0f) ? frame_time : std::min(s_minimum_frame_time_accumulator, frame_time); s_average_frame_time_accumulator += frame_time; s_maximum_frame_time_accumulator = std::max(s_maximum_frame_time_accumulator, frame_time); s_frame_time_history[s_frame_time_history_pos] = frame_time; s_frame_time_history_pos = (s_frame_time_history_pos + 1) % NUM_FRAME_TIME_SAMPLES; // update fps counter const Common::Timer::Value now_ticks = Common::Timer::GetCurrentValue(); const Common::Timer::Value ticks_diff = now_ticks - s_fps_timer.GetStartValue(); const float time = static_cast(Common::Timer::ConvertValueToSeconds(ticks_diff)); if (time < PERFORMANCE_COUNTER_UPDATE_INTERVAL) return; const float frames_run = static_cast(s_frame_number - s_last_frame_number); const u32 global_tick_counter = TimingEvents::GetGlobalTickCounter(); // TODO: Make the math here less rubbish const double pct_divider = 100.0 * (1.0 / ((static_cast(ticks_diff) * static_cast(Threading::GetThreadTicksPerSecond())) / Common::Timer::GetFrequency() / 1000000000.0)); const double time_divider = 1000.0 * (1.0 / static_cast(Threading::GetThreadTicksPerSecond())) * (1.0 / static_cast(frames_run)); s_minimum_frame_time = std::exchange(s_minimum_frame_time_accumulator, 0.0f); s_average_frame_time = std::exchange(s_average_frame_time_accumulator, 0.0f) / frames_run; s_maximum_frame_time = std::exchange(s_maximum_frame_time_accumulator, 0.0f); s_vps = static_cast(frames_run / time); s_last_frame_number = s_frame_number; s_fps = static_cast(s_internal_frame_number - s_last_internal_frame_number) / time; s_last_internal_frame_number = s_internal_frame_number; s_speed = static_cast(static_cast(global_tick_counter - s_last_global_tick_counter) / (static_cast(g_ticks_per_second) * time)) * 100.0f; s_last_global_tick_counter = global_tick_counter; const Threading::Thread* sw_thread = g_gpu->GetSWThread(); const u64 cpu_time = s_cpu_thread_handle ? s_cpu_thread_handle.GetCPUTime() : 0; const u64 sw_time = sw_thread ? sw_thread->GetCPUTime() : 0; const u64 cpu_delta = cpu_time - s_last_cpu_time; const u64 sw_delta = sw_time - s_last_sw_time; s_last_cpu_time = cpu_time; s_last_sw_time = sw_time; s_cpu_thread_usage = static_cast(static_cast(cpu_delta) * pct_divider); s_cpu_thread_time = static_cast(static_cast(cpu_delta) * time_divider); s_sw_thread_usage = static_cast(static_cast(sw_delta) * pct_divider); s_sw_thread_time = static_cast(static_cast(sw_delta) * time_divider); s_fps_timer.ResetTo(now_ticks); if (g_gpu_device->IsGPUTimingEnabled()) { s_average_gpu_time = s_accumulated_gpu_time / static_cast(std::max(s_presents_since_last_update, 1u)); s_gpu_usage = s_accumulated_gpu_time / (time * 10.0f); } s_accumulated_gpu_time = 0.0f; s_presents_since_last_update = 0; Log_VerbosePrintf("FPS: %.2f VPS: %.2f CPU: %.2f GPU: %.2f Average: %.2fms Min: %.2fms Max: %.2f ms", s_fps, s_vps, s_cpu_thread_usage, s_gpu_usage, s_average_frame_time, s_minimum_frame_time, s_maximum_frame_time); Host::OnPerformanceCountersUpdated(); } void System::ResetPerformanceCounters() { s_last_frame_number = s_frame_number; s_last_internal_frame_number = s_internal_frame_number; s_last_global_tick_counter = TimingEvents::GetGlobalTickCounter(); s_last_cpu_time = s_cpu_thread_handle ? s_cpu_thread_handle.GetCPUTime() : 0; if (const Threading::Thread* sw_thread = g_gpu->GetSWThread(); sw_thread) s_last_sw_time = sw_thread->GetCPUTime(); else s_last_sw_time = 0; s_average_frame_time_accumulator = 0.0f; s_minimum_frame_time_accumulator = 0.0f; s_maximum_frame_time_accumulator = 0.0f; s_frame_timer.Reset(); s_fps_timer.Reset(); ResetThrottler(); } void System::UpdateSpeedLimiterState() { const float old_target_speed = s_target_speed; s_target_speed = s_turbo_enabled ? g_settings.turbo_speed : (s_fast_forward_enabled ? g_settings.fast_forward_speed : g_settings.emulation_speed); s_throttler_enabled = (s_target_speed != 0.0f); s_display_all_frames = !s_throttler_enabled || g_settings.display_all_frames; s_syncing_to_host = false; if (g_settings.sync_to_host_refresh_rate && (g_settings.audio_stretch_mode != AudioStretchMode::Off) && s_target_speed == 1.0f && IsValid()) { float host_refresh_rate; if (g_gpu_device->GetHostRefreshRate(&host_refresh_rate)) { const float ratio = host_refresh_rate / System::GetThrottleFrequency(); s_syncing_to_host = (ratio >= 0.95f && ratio <= 1.05f); Log_InfoPrintf("Refresh rate: Host=%fhz Guest=%fhz Ratio=%f - %s", host_refresh_rate, System::GetThrottleFrequency(), ratio, s_syncing_to_host ? "can sync" : "can't sync"); if (s_syncing_to_host) s_target_speed *= ratio; } } // When syncing to host and using vsync, we don't need to sleep. if (s_syncing_to_host && ShouldUseVSync() && s_display_all_frames) { Log_InfoPrintf("Using host vsync for throttling."); s_throttler_enabled = false; } Log_VerbosePrintf("Target speed: %f%%", s_target_speed * 100.0f); if (IsValid()) { // Update audio output. AudioStream* stream = SPU::GetOutputStream(); stream->SetOutputVolume(GetAudioOutputVolume()); // Adjust nominal rate when resampling, or syncing to host. const bool rate_adjust = (s_syncing_to_host || g_settings.audio_stretch_mode == AudioStretchMode::Resample) && s_target_speed > 0.0f; stream->SetNominalRate(rate_adjust ? s_target_speed : 1.0f); if (old_target_speed < s_target_speed) stream->UpdateTargetTempo(s_target_speed); UpdateThrottlePeriod(); ResetThrottler(); } // Defer vsync update until we unpause, in case of fullscreen UI. if (IsRunning()) UpdateDisplaySync(); if (g_settings.increase_timer_resolution) SetTimerResolutionIncreased(s_throttler_enabled); } void System::UpdateDisplaySync() { const bool video_sync_enabled = ShouldUseVSync(); const bool syncing_to_host_vsync = (s_syncing_to_host && video_sync_enabled && s_display_all_frames); const float max_display_fps = (s_throttler_enabled || s_syncing_to_host) ? 0.0f : g_settings.display_max_fps; Log_VerbosePrintf("Using vsync: %s%s", video_sync_enabled ? "YES" : "NO", syncing_to_host_vsync ? " (for throttling)" : ""); Log_VerbosePrintf("Max display fps: %f (%s)", max_display_fps, s_display_all_frames ? "displaying all frames" : "skipping displaying frames when needed"); g_gpu_device->SetDisplayMaxFPS(max_display_fps); g_gpu_device->SetVSync(video_sync_enabled); } bool System::ShouldUseVSync() { return g_settings.video_sync_enabled && !IsRunningAtNonStandardSpeed(); } bool System::IsFastForwardEnabled() { return s_fast_forward_enabled; } void System::SetFastForwardEnabled(bool enabled) { if (!IsValid()) return; s_fast_forward_enabled = enabled; UpdateSpeedLimiterState(); } bool System::IsTurboEnabled() { return s_turbo_enabled; } void System::SetTurboEnabled(bool enabled) { if (!IsValid()) return; s_turbo_enabled = enabled; UpdateSpeedLimiterState(); } void System::SetRewindState(bool enabled) { if (!System::IsValid()) return; if (!g_settings.rewind_enable) { if (enabled) Host::AddKeyedOSDMessage("SetRewindState", TRANSLATE_STR("OSDMessage", "Rewinding is not enabled."), 5.0f); return; } if (Achievements::IsHardcoreModeActive() && enabled) { Achievements::ConfirmHardcoreModeDisableAsync("Rewinding", [](bool approved) { if (approved) SetRewindState(true); }); return; } System::SetRewinding(enabled); UpdateSpeedLimiterState(); } void System::DoFrameStep() { if (!IsValid()) return; if (Achievements::IsHardcoreModeActive()) { Achievements::ConfirmHardcoreModeDisableAsync("Frame stepping", [](bool approved) { if (approved) DoFrameStep(); }); return; } s_frame_step_request = true; PauseSystem(false); } void System::DoToggleCheats() { if (!System::IsValid()) return; if (Achievements::IsHardcoreModeActive()) { Achievements::ConfirmHardcoreModeDisableAsync("Toggling cheats", [](bool approved) { DoToggleCheats(); }); return; } CheatList* cl = GetCheatList(); if (!cl) { Host::AddKeyedOSDMessage("ToggleCheats", TRANSLATE_STR("OSDMessage", "No cheats are loaded."), 10.0f); return; } cl->SetMasterEnable(!cl->GetMasterEnable()); Host::AddKeyedOSDMessage( "ToggleCheats", cl->GetMasterEnable() ? fmt::format(TRANSLATE_FS("OSDMessage", "{} cheats are now active."), cl->GetEnabledCodeCount()) : fmt::format(TRANSLATE_FS("OSDMessage", "{} cheats are now inactive."), cl->GetEnabledCodeCount()), 10.0f); } static bool LoadEXEToRAM(const char* filename, bool patch_bios) { std::FILE* fp = FileSystem::OpenCFile(filename, "rb"); if (!fp) { Log_ErrorPrintf("Failed to open exe file '%s'", filename); return false; } std::fseek(fp, 0, SEEK_END); const u32 file_size = static_cast(std::ftell(fp)); std::fseek(fp, 0, SEEK_SET); BIOS::PSEXEHeader header; if (std::fread(&header, sizeof(header), 1, fp) != 1 || !BIOS::IsValidPSExeHeader(header, file_size)) { Log_ErrorPrintf("'%s' is not a valid PS-EXE", filename); std::fclose(fp); return false; } if (header.memfill_size > 0) { const u32 words_to_write = header.memfill_size / 4; u32 address = header.memfill_start & ~UINT32_C(3); for (u32 i = 0; i < words_to_write; i++) { CPU::SafeWriteMemoryWord(address, 0); address += sizeof(u32); } } const u32 file_data_size = std::min(file_size - sizeof(BIOS::PSEXEHeader), header.file_size); if (file_data_size >= 4) { std::vector data_words((file_data_size + 3) / 4); if (std::fread(data_words.data(), file_data_size, 1, fp) != 1) { std::fclose(fp); return false; } const u32 num_words = file_data_size / 4; u32 address = header.load_address; for (u32 i = 0; i < num_words; i++) { CPU::SafeWriteMemoryWord(address, data_words[i]); address += sizeof(u32); } } std::fclose(fp); // patch the BIOS to jump to the executable directly const u32 r_pc = header.initial_pc; const u32 r_gp = header.initial_gp; const u32 r_sp = header.initial_sp_base + header.initial_sp_offset; const u32 r_fp = header.initial_sp_base + header.initial_sp_offset; return BIOS::PatchBIOSForEXE(Bus::g_bios, Bus::BIOS_SIZE, r_pc, r_gp, r_sp, r_fp); } bool System::LoadEXE(const char* filename) { const std::string libps_path(Path::BuildRelativePath(filename, "libps.exe")); if (!libps_path.empty() && FileSystem::FileExists(libps_path.c_str()) && !LoadEXEToRAM(libps_path.c_str(), false)) { Log_ErrorPrintf("Failed to load libps.exe from '%s'", libps_path.c_str()); return false; } return LoadEXEToRAM(filename, true); } bool System::InjectEXEFromBuffer(const void* buffer, u32 buffer_size, bool patch_bios) { const u8* buffer_ptr = static_cast(buffer); const u8* buffer_end = static_cast(buffer) + buffer_size; BIOS::PSEXEHeader header; if (buffer_size < sizeof(header)) return false; std::memcpy(&header, buffer_ptr, sizeof(header)); buffer_ptr += sizeof(header); const u32 file_size = static_cast(static_cast(buffer_end - buffer_ptr)); if (!BIOS::IsValidPSExeHeader(header, file_size)) return false; if (header.memfill_size > 0) { const u32 words_to_write = header.memfill_size / 4; u32 address = header.memfill_start & ~UINT32_C(3); for (u32 i = 0; i < words_to_write; i++) { CPU::SafeWriteMemoryWord(address, 0); address += sizeof(u32); } } const u32 file_data_size = std::min(file_size - sizeof(BIOS::PSEXEHeader), header.file_size); if (file_data_size >= 4) { std::vector data_words((file_data_size + 3) / 4); if ((buffer_end - buffer_ptr) < file_data_size) return false; std::memcpy(data_words.data(), buffer_ptr, file_data_size); const u32 num_words = file_data_size / 4; u32 address = header.load_address; for (u32 i = 0; i < num_words; i++) { CPU::SafeWriteMemoryWord(address, data_words[i]); address += sizeof(u32); } } // patch the BIOS to jump to the executable directly if (patch_bios) { const u32 r_pc = header.initial_pc; const u32 r_gp = header.initial_gp; const u32 r_sp = header.initial_sp_base + header.initial_sp_offset; const u32 r_fp = header.initial_sp_base + header.initial_sp_offset; if (!BIOS::PatchBIOSForEXE(Bus::g_bios, Bus::BIOS_SIZE, r_pc, r_gp, r_sp, r_fp)) return false; } return true; } #if 0 // currently not used until EXP1 is implemented bool SetExpansionROM(const char* filename) { std::FILE* fp = FileSystem::OpenCFile(filename, "rb"); if (!fp) { Log_ErrorPrintf("Failed to open '%s'", filename); return false; } std::fseek(fp, 0, SEEK_END); const u32 size = static_cast(std::ftell(fp)); std::fseek(fp, 0, SEEK_SET); std::vector data(size); if (std::fread(data.data(), size, 1, fp) != 1) { Log_ErrorPrintf("Failed to read ROM data from '%s'", filename); std::fclose(fp); return false; } std::fclose(fp); Log_InfoPrintf("Loaded expansion ROM from '%s': %u bytes", filename, size); Bus::SetExpansionROM(std::move(data)); return true; } #endif Controller* System::GetController(u32 slot) { return Pad::GetController(slot); } void System::UpdateControllers() { auto lock = Host::GetSettingsLock(); for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { Pad::SetController(i, nullptr); const ControllerType type = g_settings.controller_types[i]; if (type != ControllerType::None) { std::unique_ptr controller = Controller::Create(type, i); if (controller) { controller->LoadSettings(*Host::GetSettingsInterfaceForBindings(), Controller::GetSettingsSection(i).c_str()); Pad::SetController(i, std::move(controller)); } } } } void System::UpdateControllerSettings() { auto lock = Host::GetSettingsLock(); for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { Controller* controller = Pad::GetController(i); if (controller) controller->LoadSettings(*Host::GetSettingsInterfaceForBindings(), Controller::GetSettingsSection(i).c_str()); } } void System::ResetControllers() { for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { Controller* controller = Pad::GetController(i); if (controller) controller->Reset(); } } std::unique_ptr System::GetMemoryCardForSlot(u32 slot, MemoryCardType type) { // Disable memory cards when running PSFs. const bool is_running_psf = !s_running_game_path.empty() && IsPsfFileName(s_running_game_path.c_str()); if (is_running_psf) return nullptr; std::string message_key = fmt::format("MemoryCard{}SharedWarning", slot); switch (type) { case MemoryCardType::PerGame: { if (s_running_game_serial.empty()) { Host::AddIconOSDMessage( std::move(message_key), ICON_FA_SD_CARD, fmt::format(TRANSLATE_FS("System", "Per-game memory card cannot be used for slot {} as the running " "game has no code. Using shared card instead."), slot + 1u), Host::OSD_INFO_DURATION); return MemoryCard::Open(g_settings.GetSharedMemoryCardPath(slot)); } else { Host::RemoveKeyedOSDMessage(std::move(message_key)); return MemoryCard::Open(g_settings.GetGameMemoryCardPath(s_running_game_serial.c_str(), slot)); } } case MemoryCardType::PerGameTitle: { if (s_running_game_title.empty()) { Host::AddIconOSDMessage( std::move(message_key), ICON_FA_SD_CARD, fmt::format(TRANSLATE_FS("System", "Per-game memory card cannot be used for slot {} as the running " "game has no title. Using shared card instead."), slot + 1u), Host::OSD_INFO_DURATION); return MemoryCard::Open(g_settings.GetSharedMemoryCardPath(slot)); } else { std::string card_path; // Playlist - use title if different. if (HasMediaSubImages() && s_running_game_entry && s_running_game_title != s_running_game_entry->title) { card_path = g_settings.GetGameMemoryCardPath(MemoryCard::SanitizeGameTitleForFileName(s_running_game_title), slot); } // Multi-disc game - use disc set name. else if (s_running_game_entry && !s_running_game_entry->disc_set_name.empty()) { card_path = g_settings.GetGameMemoryCardPath( MemoryCard::SanitizeGameTitleForFileName(s_running_game_entry->disc_set_name), slot); } // But prefer a disc-specific card if one already exists. std::string disc_card_path = g_settings.GetGameMemoryCardPath(MemoryCard::SanitizeGameTitleForFileName( s_running_game_entry ? s_running_game_entry->title : s_running_game_title), slot); if (disc_card_path != card_path) { if (card_path.empty() || !g_settings.memory_card_use_playlist_title || FileSystem::FileExists(disc_card_path.c_str())) { if (g_settings.memory_card_use_playlist_title && !card_path.empty()) { Host::AddIconOSDMessage( fmt::format("DiscSpecificMC{}", slot), ICON_FA_SD_CARD, fmt::format(TRANSLATE_FS("System", "Using disc-specific memory card '{}' instead of per-game card."), Path::GetFileName(disc_card_path)), Host::OSD_INFO_DURATION); } card_path = std::move(disc_card_path); } } Host::RemoveKeyedOSDMessage(std::move(message_key)); return MemoryCard::Open(card_path.c_str()); } } case MemoryCardType::PerGameFileTitle: { const std::string display_name(FileSystem::GetDisplayNameFromPath(s_running_game_path)); const std::string_view file_title(Path::GetFileTitle(display_name)); if (file_title.empty()) { Host::AddIconOSDMessage( std::move(message_key), ICON_FA_SD_CARD, fmt::format(TRANSLATE_FS("System", "Per-game memory card cannot be used for slot {} as the running " "game has no path. Using shared card instead."), slot + 1u)); return MemoryCard::Open(g_settings.GetSharedMemoryCardPath(slot)); } else { Host::RemoveKeyedOSDMessage(std::move(message_key)); return MemoryCard::Open( g_settings.GetGameMemoryCardPath(MemoryCard::SanitizeGameTitleForFileName(file_title).c_str(), slot)); } } case MemoryCardType::Shared: { Host::RemoveKeyedOSDMessage(std::move(message_key)); return MemoryCard::Open(g_settings.GetSharedMemoryCardPath(slot)); } case MemoryCardType::NonPersistent: { Host::RemoveKeyedOSDMessage(std::move(message_key)); return MemoryCard::Create(); } case MemoryCardType::None: default: { Host::RemoveKeyedOSDMessage(std::move(message_key)); return nullptr; } } } void System::UpdateMemoryCardTypes() { for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { Pad::SetMemoryCard(i, nullptr); const MemoryCardType type = g_settings.memory_card_types[i]; std::unique_ptr card = GetMemoryCardForSlot(i, type); if (card) { if (const std::string& filename = card->GetFilename(); !filename.empty()) Log_InfoFmt("Memory Card Slot {}: {}", i + 1, filename); Pad::SetMemoryCard(i, std::move(card)); } } } void System::UpdatePerGameMemoryCards() { for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { const MemoryCardType type = g_settings.memory_card_types[i]; if (!Settings::IsPerGameMemoryCardType(type)) continue; Pad::SetMemoryCard(i, nullptr); std::unique_ptr card = GetMemoryCardForSlot(i, type); if (card) { if (const std::string& filename = card->GetFilename(); !filename.empty()) Log_InfoFmt("Memory Card Slot {}: {}", i + 1, filename); Pad::SetMemoryCard(i, std::move(card)); } } } bool System::HasMemoryCard(u32 slot) { return (Pad::GetMemoryCard(slot) != nullptr); } void System::SwapMemoryCards() { if (!IsValid()) return; std::unique_ptr first = Pad::RemoveMemoryCard(0); std::unique_ptr second = Pad::RemoveMemoryCard(1); Pad::SetMemoryCard(0, std::move(second)); Pad::SetMemoryCard(1, std::move(first)); if (HasMemoryCard(0) && HasMemoryCard(1)) { Host::AddOSDMessage(TRANSLATE_STR("OSDMessage", "Swapped memory card ports. Both ports have a memory card."), 10.0f); } else if (HasMemoryCard(1)) { Host::AddOSDMessage( TRANSLATE_STR("OSDMessage", "Swapped memory card ports. Port 2 has a memory card, Port 1 is empty."), 10.0f); } else if (HasMemoryCard(0)) { Host::AddOSDMessage( TRANSLATE_STR("OSDMessage", "Swapped memory card ports. Port 1 has a memory card, Port 2 is empty."), 10.0f); } else { Host::AddOSDMessage(TRANSLATE_STR("OSDMessage", "Swapped memory card ports. Neither port has a memory card."), 10.0f); } } void System::UpdateMultitaps() { switch (g_settings.multitap_mode) { case MultitapMode::Disabled: { Pad::GetMultitap(0)->SetEnable(false, 0); Pad::GetMultitap(1)->SetEnable(false, 0); } break; case MultitapMode::Port1Only: { Pad::GetMultitap(0)->SetEnable(true, 0); Pad::GetMultitap(1)->SetEnable(false, 0); } break; case MultitapMode::Port2Only: { Pad::GetMultitap(0)->SetEnable(false, 0); Pad::GetMultitap(1)->SetEnable(true, 1); } break; case MultitapMode::BothPorts: { Pad::GetMultitap(0)->SetEnable(true, 0); Pad::GetMultitap(1)->SetEnable(true, 4); } break; default: UnreachableCode(); break; } } bool System::DumpRAM(const char* filename) { if (!IsValid()) return false; return FileSystem::WriteBinaryFile(filename, Bus::g_unprotected_ram, Bus::g_ram_size); } bool System::DumpVRAM(const char* filename) { if (!IsValid()) return false; g_gpu->RestoreDeviceContext(); return g_gpu->DumpVRAMToFile(filename); } bool System::DumpSPURAM(const char* filename) { if (!IsValid()) return false; return FileSystem::WriteBinaryFile(filename, SPU::GetRAM().data(), SPU::RAM_SIZE); } bool System::HasMedia() { return CDROM::HasMedia(); } std::string System::GetMediaFileName() { if (!CDROM::HasMedia()) return {}; return CDROM::GetMediaFileName(); } bool System::InsertMedia(const char* path) { Error error; std::unique_ptr image = CDImage::Open(path, g_settings.cdrom_load_image_patches, &error); if (!image) { Host::AddIconOSDMessage( "DiscInserted", ICON_FA_COMPACT_DISC, fmt::format(TRANSLATE_FS("OSDMessage", "Failed to open disc image '{}': {}."), path, error.GetDescription()), Host::OSD_ERROR_DURATION); return false; } const DiscRegion region = GetRegionForImage(image.get()); UpdateRunningGame(path, image.get(), false); CDROM::InsertMedia(std::move(image), region); Log_InfoFmt("Inserted media from {} ({}, {})", s_running_game_path, s_running_game_serial, s_running_game_title); if (g_settings.cdrom_load_image_to_ram) CDROM::PrecacheMedia(); Host::AddIconOSDMessage( "DiscInserted", ICON_FA_COMPACT_DISC, fmt::format(TRANSLATE_FS("OSDMessage", "Inserted disc '{}' ({})."), s_running_game_title, s_running_game_serial), Host::OSD_INFO_DURATION); if (g_settings.HasAnyPerGameMemoryCards()) { Host::AddIconOSDMessage("ReloadMemoryCardsFromGameChange", ICON_FA_SD_CARD, TRANSLATE_STR("System", "Game changed, reloading memory cards."), Host::OSD_INFO_DURATION); UpdatePerGameMemoryCards(); } ClearMemorySaveStates(); return true; } void System::RemoveMedia() { CDROM::RemoveMedia(false); ClearMemorySaveStates(); } void System::UpdateRunningGame(const char* path, CDImage* image, bool booting) { if (!booting && s_running_game_path == path) return; const std::string prev_serial = std::move(s_running_game_serial); s_running_game_path.clear(); s_running_game_serial = {}; s_running_game_title.clear(); s_running_game_entry = nullptr; s_running_game_hash = 0; if (path && std::strlen(path) > 0) { s_running_game_path = path; if (IsExeFileName(path) || IsPsfFileName(path)) { // TODO: We could pull the title from the PSF. s_running_game_title = Path::GetFileTitle(path); } else if (image) { std::string id; GetGameDetailsFromImage(image, &id, &s_running_game_hash); s_running_game_entry = GameDatabase::GetEntryForGameDetails(id, s_running_game_hash); if (s_running_game_entry) { s_running_game_serial = s_running_game_entry->serial; s_running_game_title = s_running_game_entry->title; } else { s_running_game_serial = std::move(id); s_running_game_title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path)); } if (image->HasSubImages()) { std::string image_title = image->GetMetadata("title"); if (!image_title.empty()) s_running_game_title = std::move(image_title); } } } g_texture_replacements.SetGameID(s_running_game_serial); if (booting) Achievements::ResetHardcoreMode(); Achievements::GameChanged(s_running_game_path, image); UpdateGameSettingsLayer(); ApplySettings(true); s_cheat_list.reset(); if (g_settings.auto_load_cheats && !Achievements::IsHardcoreModeActive()) LoadCheatListFromGameTitle(); if (s_running_game_serial != prev_serial) UpdateSessionTime(prev_serial); SaveStateSelectorUI::RefreshList(); #ifdef ENABLE_DISCORD_PRESENCE UpdateDiscordPresence(); #endif Host::OnGameChanged(s_running_game_path, s_running_game_serial, s_running_game_title); } bool System::CheckForSBIFile(CDImage* image) { if (!s_running_game_entry || !s_running_game_entry->HasTrait(GameDatabase::Trait::IsLibCryptProtected) || !image || image->HasNonStandardSubchannel()) { return true; } Log_WarningPrintf("SBI file missing but required for %s (%s)", s_running_game_serial.c_str(), s_running_game_title.c_str()); if (Host::GetBoolSettingValue("CDROM", "AllowBootingWithoutSBIFile", false)) { return Host::ConfirmMessage( "Confirm Unsupported Configuration", LargeString::from_fmt( TRANSLATE_FS("System", "You are attempting to run a libcrypt protected game without an SBI file:\n\n{0}: " "{1}\n\nThe game will likely not run properly.\n\nPlease check the README for " "instructions on how to add an SBI file.\n\nDo you wish to continue?"), s_running_game_serial, s_running_game_title)); } else { Host::ReportErrorAsync( TRANSLATE("System", "Error"), LargeString::from_fmt( TRANSLATE_FS("System", "You are attempting to run a libcrypt protected game without an SBI file:\n\n{0}: " "{1}\n\nYour dump is incomplete, you must add the SBI file to run this game. \n\nThe " "name of the SBI file must match the name of the disc image."), s_running_game_serial, s_running_game_title)); return false; } } bool System::HasMediaSubImages() { const CDImage* cdi = CDROM::GetMedia(); return cdi ? cdi->HasSubImages() : false; } u32 System::GetMediaSubImageCount() { const CDImage* cdi = CDROM::GetMedia(); return cdi ? cdi->GetSubImageCount() : 0; } u32 System::GetMediaSubImageIndex() { const CDImage* cdi = CDROM::GetMedia(); return cdi ? cdi->GetCurrentSubImage() : 0; } u32 System::GetMediaSubImageIndexForTitle(const std::string_view& title) { const CDImage* cdi = CDROM::GetMedia(); if (!cdi) return 0; const u32 count = cdi->GetSubImageCount(); for (u32 i = 0; i < count; i++) { if (title == cdi->GetSubImageMetadata(i, "title")) return i; } return std::numeric_limits::max(); } std::string System::GetMediaSubImageTitle(u32 index) { const CDImage* cdi = CDROM::GetMedia(); if (!cdi) return {}; return cdi->GetSubImageMetadata(index, "title"); } bool System::SwitchMediaSubImage(u32 index) { if (!CDROM::HasMedia()) return false; std::unique_ptr image = CDROM::RemoveMedia(true); Assert(image); Error error; if (!image->SwitchSubImage(index, &error)) { Host::AddFormattedOSDMessage(10.0f, TRANSLATE("OSDMessage", "Failed to switch to subimage %u in '%s': %s."), index + 1u, image->GetFileName().c_str(), error.GetDescription().c_str()); const DiscRegion region = GetRegionForImage(image.get()); CDROM::InsertMedia(std::move(image), region); return false; } Host::AddFormattedOSDMessage(20.0f, TRANSLATE("OSDMessage", "Switched to sub-image %s (%u) in '%s'."), image->GetSubImageMetadata(index, "title").c_str(), index + 1u, image->GetMetadata("title").c_str()); const DiscRegion region = GetRegionForImage(image.get()); CDROM::InsertMedia(std::move(image), region); ClearMemorySaveStates(); return true; } bool System::HasCheatList() { return static_cast(s_cheat_list); } CheatList* System::GetCheatList() { return s_cheat_list.get(); } void System::ApplyCheatCode(const CheatCode& code) { Assert(!IsShutdown()); code.Apply(); } void System::SetCheatList(std::unique_ptr cheats) { Assert(!IsShutdown()); s_cheat_list = std::move(cheats); } void System::CheckForSettingsChanges(const Settings& old_settings) { if (IsValid() && (g_settings.gpu_renderer != old_settings.gpu_renderer || g_settings.gpu_use_debug_device != old_settings.gpu_use_debug_device || g_settings.gpu_threaded_presentation != old_settings.gpu_threaded_presentation || g_settings.gpu_disable_shader_cache != old_settings.gpu_disable_shader_cache || g_settings.gpu_disable_dual_source_blend != old_settings.gpu_disable_dual_source_blend || g_settings.gpu_disable_framebuffer_fetch != old_settings.gpu_disable_framebuffer_fetch)) { // if debug device/threaded presentation change, we need to recreate the whole display const bool recreate_device = (g_settings.gpu_use_debug_device != old_settings.gpu_use_debug_device || g_settings.gpu_threaded_presentation != old_settings.gpu_threaded_presentation || g_settings.gpu_disable_shader_cache != old_settings.gpu_disable_shader_cache || g_settings.gpu_disable_dual_source_blend != old_settings.gpu_disable_dual_source_blend || g_settings.gpu_disable_framebuffer_fetch != old_settings.gpu_disable_framebuffer_fetch); Host::AddIconOSDMessage("RendererSwitch", ICON_FA_PAINT_ROLLER, fmt::format(TRANSLATE_FS("OSDMessage", "Switching to {}{} GPU renderer."), Settings::GetRendererName(g_settings.gpu_renderer), g_settings.gpu_use_debug_device ? " (debug)" : ""), Host::OSD_INFO_DURATION); RecreateGPU(g_settings.gpu_renderer, recreate_device); } if (IsValid()) { ClearMemorySaveStates(); if (g_settings.cpu_overclock_active != old_settings.cpu_overclock_active || (g_settings.cpu_overclock_active && (g_settings.cpu_overclock_numerator != old_settings.cpu_overclock_numerator || g_settings.cpu_overclock_denominator != old_settings.cpu_overclock_denominator))) { UpdateOverclock(); } if (g_settings.audio_backend != old_settings.audio_backend || g_settings.audio_driver != old_settings.audio_driver || g_settings.audio_output_device != old_settings.audio_output_device) { if (g_settings.audio_backend != old_settings.audio_backend) { Host::AddIconOSDMessage("AudioBackendSwitch", ICON_FA_HEADPHONES, fmt::format(TRANSLATE_FS("OSDMessage", "Switching to {} audio backend."), Settings::GetAudioBackendName(g_settings.audio_backend)), Host::OSD_INFO_DURATION); } SPU::RecreateOutputStream(); } if (g_settings.audio_stretch_mode != old_settings.audio_stretch_mode) SPU::GetOutputStream()->SetStretchMode(g_settings.audio_stretch_mode); if (g_settings.audio_buffer_ms != old_settings.audio_buffer_ms || g_settings.audio_output_latency_ms != old_settings.audio_output_latency_ms || g_settings.audio_stretch_mode != old_settings.audio_stretch_mode) { SPU::RecreateOutputStream(); UpdateSpeedLimiterState(); } if (g_settings.emulation_speed != old_settings.emulation_speed) UpdateThrottlePeriod(); if (g_settings.cpu_execution_mode != old_settings.cpu_execution_mode || g_settings.cpu_fastmem_mode != old_settings.cpu_fastmem_mode) { Host::AddIconOSDMessage("CPUExecutionModeSwitch", ICON_FA_MICROCHIP, fmt::format(TRANSLATE_FS("OSDMessage", "Switching to {} CPU execution mode."), TRANSLATE_SV("CPUExecutionMode", Settings::GetCPUExecutionModeDisplayName( g_settings.cpu_execution_mode))), Host::OSD_INFO_DURATION); CPU::ExecutionModeChanged(); if (old_settings.cpu_execution_mode != CPUExecutionMode::Interpreter) CPU::CodeCache::Shutdown(); if (g_settings.cpu_execution_mode != CPUExecutionMode::Interpreter) CPU::CodeCache::Initialize(); CPU::ClearICache(); } if (CPU::CodeCache::IsUsingAnyRecompiler() && (g_settings.cpu_recompiler_memory_exceptions != old_settings.cpu_recompiler_memory_exceptions || g_settings.cpu_recompiler_block_linking != old_settings.cpu_recompiler_block_linking || g_settings.cpu_recompiler_icache != old_settings.cpu_recompiler_icache || g_settings.bios_tty_logging != old_settings.bios_tty_logging)) { Host::AddIconOSDMessage("CPUFlushAllBlocks", ICON_FA_MICROCHIP, TRANSLATE_STR("OSDMessage", "Recompiler options changed, flushing all blocks."), Host::OSD_INFO_DURATION); CPU::ExecutionModeChanged(); CPU::CodeCache::Reset(); if (g_settings.cpu_recompiler_icache != old_settings.cpu_recompiler_icache) CPU::ClearICache(); } SPU::GetOutputStream()->SetOutputVolume(GetAudioOutputVolume()); if (g_settings.gpu_resolution_scale != old_settings.gpu_resolution_scale || g_settings.gpu_multisamples != old_settings.gpu_multisamples || g_settings.gpu_per_sample_shading != old_settings.gpu_per_sample_shading || g_settings.gpu_use_thread != old_settings.gpu_use_thread || g_settings.gpu_use_software_renderer_for_readbacks != old_settings.gpu_use_software_renderer_for_readbacks || g_settings.gpu_fifo_size != old_settings.gpu_fifo_size || g_settings.gpu_max_run_ahead != old_settings.gpu_max_run_ahead || g_settings.gpu_true_color != old_settings.gpu_true_color || g_settings.gpu_scaled_dithering != old_settings.gpu_scaled_dithering || g_settings.gpu_texture_filter != old_settings.gpu_texture_filter || g_settings.gpu_disable_interlacing != old_settings.gpu_disable_interlacing || g_settings.gpu_force_ntsc_timings != old_settings.gpu_force_ntsc_timings || g_settings.gpu_24bit_chroma_smoothing != old_settings.gpu_24bit_chroma_smoothing || g_settings.gpu_downsample_mode != old_settings.gpu_downsample_mode || g_settings.gpu_downsample_scale != old_settings.gpu_downsample_scale || g_settings.gpu_wireframe_mode != old_settings.gpu_wireframe_mode || g_settings.display_crop_mode != old_settings.display_crop_mode || g_settings.display_aspect_ratio != old_settings.display_aspect_ratio || g_settings.display_alignment != old_settings.display_alignment || g_settings.display_scaling != old_settings.display_scaling || g_settings.display_show_gpu != old_settings.display_show_gpu || g_settings.gpu_pgxp_enable != old_settings.gpu_pgxp_enable || g_settings.gpu_pgxp_texture_correction != old_settings.gpu_pgxp_texture_correction || g_settings.gpu_pgxp_color_correction != old_settings.gpu_pgxp_color_correction || g_settings.gpu_pgxp_depth_buffer != old_settings.gpu_pgxp_depth_buffer || g_settings.display_active_start_offset != old_settings.display_active_start_offset || g_settings.display_active_end_offset != old_settings.display_active_end_offset || g_settings.display_line_start_offset != old_settings.display_line_start_offset || g_settings.display_line_end_offset != old_settings.display_line_end_offset || g_settings.rewind_enable != old_settings.rewind_enable || g_settings.runahead_frames != old_settings.runahead_frames) { g_gpu->UpdateSettings(old_settings); InvalidateDisplay(); } if (g_settings.gpu_widescreen_hack != old_settings.gpu_widescreen_hack || g_settings.display_aspect_ratio != old_settings.display_aspect_ratio || (g_settings.display_aspect_ratio == DisplayAspectRatio::Custom && (g_settings.display_aspect_ratio_custom_numerator != old_settings.display_aspect_ratio_custom_numerator || g_settings.display_aspect_ratio_custom_denominator != old_settings.display_aspect_ratio_custom_denominator))) { GTE::UpdateAspectRatio(); } if (g_settings.gpu_pgxp_enable != old_settings.gpu_pgxp_enable || (g_settings.gpu_pgxp_enable && (g_settings.gpu_pgxp_culling != old_settings.gpu_pgxp_culling || g_settings.gpu_pgxp_vertex_cache != old_settings.gpu_pgxp_vertex_cache || g_settings.gpu_pgxp_cpu != old_settings.gpu_pgxp_cpu))) { if (old_settings.gpu_pgxp_enable) PGXP::Shutdown(); if (g_settings.gpu_pgxp_enable) PGXP::Initialize(); CPU::CodeCache::Reset(); } if (g_settings.cdrom_readahead_sectors != old_settings.cdrom_readahead_sectors) CDROM::SetReadaheadSectors(g_settings.cdrom_readahead_sectors); if (g_settings.memory_card_types != old_settings.memory_card_types || g_settings.memory_card_paths != old_settings.memory_card_paths || (g_settings.memory_card_use_playlist_title != old_settings.memory_card_use_playlist_title)) { UpdateMemoryCardTypes(); } if (g_settings.rewind_enable != old_settings.rewind_enable || g_settings.rewind_save_frequency != old_settings.rewind_save_frequency || g_settings.rewind_save_slots != old_settings.rewind_save_slots || g_settings.runahead_frames != old_settings.runahead_frames) { UpdateMemorySaveStateSettings(); } if (g_settings.texture_replacements.enable_vram_write_replacements != old_settings.texture_replacements.enable_vram_write_replacements || g_settings.texture_replacements.preload_textures != old_settings.texture_replacements.preload_textures) { g_texture_replacements.Reload(); } DMA::SetMaxSliceTicks(g_settings.dma_max_slice_ticks); DMA::SetHaltTicks(g_settings.dma_halt_ticks); if (g_settings.audio_backend != old_settings.audio_backend || g_settings.video_sync_enabled != old_settings.video_sync_enabled || g_settings.increase_timer_resolution != old_settings.increase_timer_resolution || g_settings.emulation_speed != old_settings.emulation_speed || g_settings.fast_forward_speed != old_settings.fast_forward_speed || g_settings.display_max_fps != old_settings.display_max_fps || g_settings.display_all_frames != old_settings.display_all_frames || g_settings.sync_to_host_refresh_rate != old_settings.sync_to_host_refresh_rate) { UpdateSpeedLimiterState(); } if (g_settings.inhibit_screensaver != old_settings.inhibit_screensaver) { if (g_settings.inhibit_screensaver) PlatformMisc::SuspendScreensaver(); else PlatformMisc::ResumeScreensaver(); } PostProcessing::UpdateSettings(); } if (g_gpu_device) { if (g_settings.display_osd_scale != old_settings.display_osd_scale) ImGuiManager::SetGlobalScale(g_settings.display_osd_scale / 100.0f); if (g_settings.display_show_osd_messages != old_settings.display_show_osd_messages) ImGuiManager::SetShowOSDMessages(g_settings.display_show_osd_messages); } bool controllers_updated = false; for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { if (g_settings.controller_types[i] != old_settings.controller_types[i]) { if (IsValid() && !controllers_updated) { UpdateControllers(); ResetControllers(); controllers_updated = true; } } } if (IsValid() && !controllers_updated) UpdateControllerSettings(); if (g_settings.multitap_mode != old_settings.multitap_mode) UpdateMultitaps(); Achievements::UpdateSettings(old_settings); FullscreenUI::CheckForConfigChanges(old_settings); #ifdef ENABLE_DISCORD_PRESENCE if (g_settings.enable_discord_presence != old_settings.enable_discord_presence) { if (g_settings.enable_discord_presence) InitializeDiscordPresence(); else ShutdownDiscordPresence(); } #endif if (g_settings.log_level != old_settings.log_level || g_settings.log_filter != old_settings.log_filter || g_settings.log_timestamps != old_settings.log_timestamps || g_settings.log_to_console != old_settings.log_to_console || g_settings.log_to_debug != old_settings.log_to_debug || g_settings.log_to_window != old_settings.log_to_window || g_settings.log_to_file != old_settings.log_to_file) { g_settings.UpdateLogSettings(); } } void System::CalculateRewindMemoryUsage(u32 num_saves, u64* ram_usage, u64* vram_usage) { *ram_usage = MAX_SAVE_STATE_SIZE * static_cast(num_saves); *vram_usage = (VRAM_WIDTH * VRAM_HEIGHT * 4) * static_cast(std::max(g_settings.gpu_resolution_scale, 1u)) * static_cast(g_settings.gpu_multisamples) * static_cast(num_saves); } void System::ClearMemorySaveStates() { s_rewind_states.clear(); s_runahead_states.clear(); } void System::UpdateMemorySaveStateSettings() { ClearMemorySaveStates(); s_memory_saves_enabled = g_settings.rewind_enable; if (g_settings.rewind_enable) { s_rewind_save_frequency = static_cast(std::ceil(g_settings.rewind_save_frequency * s_throttle_frequency)); s_rewind_save_counter = 0; u64 ram_usage, vram_usage; CalculateRewindMemoryUsage(g_settings.rewind_save_slots, &ram_usage, &vram_usage); Log_InfoPrintf( "Rewind is enabled, saving every %d frames, with %u slots and %" PRIu64 "MB RAM and %" PRIu64 "MB VRAM usage", std::max(s_rewind_save_frequency, 1), g_settings.rewind_save_slots, ram_usage / 1048576, vram_usage / 1048576); } else { s_rewind_save_frequency = -1; s_rewind_save_counter = -1; } s_rewind_load_frequency = -1; s_rewind_load_counter = -1; s_runahead_frames = g_settings.runahead_frames; s_runahead_replay_pending = false; if (s_runahead_frames > 0) Log_InfoPrintf("Runahead is active with %u frames", s_runahead_frames); } bool System::LoadMemoryState(const MemorySaveState& mss) { mss.state_stream->SeekAbsolute(0); StateWrapper sw(mss.state_stream.get(), StateWrapper::Mode::Read, SAVE_STATE_VERSION); GPUTexture* host_texture = mss.vram_texture.get(); if (!DoState(sw, &host_texture, true, true)) { Host::ReportErrorAsync("Error", "Failed to load memory save state, resetting."); InternalReset(); return false; } return true; } bool System::SaveMemoryState(MemorySaveState* mss) { if (!mss->state_stream) mss->state_stream = std::make_unique(nullptr, MAX_SAVE_STATE_SIZE); else mss->state_stream->SeekAbsolute(0); GPUTexture* host_texture = mss->vram_texture.release(); StateWrapper sw(mss->state_stream.get(), StateWrapper::Mode::Write, SAVE_STATE_VERSION); if (!DoState(sw, &host_texture, false, true)) { Log_ErrorPrint("Failed to create rewind state."); delete host_texture; return false; } mss->vram_texture.reset(host_texture); return true; } bool System::SaveRewindState() { #ifdef PROFILE_MEMORY_SAVE_STATES Common::Timer save_timer; #endif // try to reuse the frontmost slot const u32 save_slots = g_settings.rewind_save_slots; MemorySaveState mss; while (s_rewind_states.size() >= save_slots) { mss = std::move(s_rewind_states.front()); s_rewind_states.pop_front(); } if (!SaveMemoryState(&mss)) return false; s_rewind_states.push_back(std::move(mss)); #ifdef PROFILE_MEMORY_SAVE_STATES Log_DevPrintf("Saved rewind state (%" PRIu64 " bytes, took %.4f ms)", s_rewind_states.back().state_stream->GetSize(), save_timer.GetTimeMilliseconds()); #endif return true; } bool System::LoadRewindState(u32 skip_saves /*= 0*/, bool consume_state /*=true */) { while (skip_saves > 0 && !s_rewind_states.empty()) { s_rewind_states.pop_back(); skip_saves--; } if (s_rewind_states.empty()) return false; #ifdef PROFILE_MEMORY_SAVE_STATES Common::Timer load_timer; #endif if (!LoadMemoryState(s_rewind_states.back())) return false; if (consume_state) s_rewind_states.pop_back(); #ifdef PROFILE_MEMORY_SAVE_STATES Log_DevPrintf("Rewind load took %.4f ms", load_timer.GetTimeMilliseconds()); #endif return true; } bool System::IsRewinding() { return (s_rewind_load_frequency >= 0); } void System::SetRewinding(bool enabled) { if (enabled) { const bool was_enabled = IsRewinding(); // Try to rewind at the replay speed, or one per second maximum. const float load_frequency = std::min(g_settings.rewind_save_frequency, 1.0f); s_rewind_load_frequency = static_cast(std::ceil(load_frequency * s_throttle_frequency)); s_rewind_load_counter = 0; if (!was_enabled && s_system_executing) s_system_interrupted = true; } else { s_rewind_load_frequency = -1; s_rewind_load_counter = -1; s_rewinding_first_save = true; } } void System::DoRewind() { if (s_rewind_load_counter == 0) { const u32 skip_saves = BoolToUInt32(!s_rewinding_first_save); s_rewinding_first_save = false; LoadRewindState(skip_saves, false); ResetPerformanceCounters(); s_rewind_load_counter = s_rewind_load_frequency; } else { s_rewind_load_counter--; } s_next_frame_time += s_frame_period; InvalidateDisplay(); Host::PumpMessagesOnCPUThread(); Internal::IdlePollUpdate(); Throttle(); } void System::SaveRunaheadState() { // try to reuse the frontmost slot MemorySaveState mss; while (s_runahead_states.size() >= s_runahead_frames) { mss = std::move(s_runahead_states.front()); s_runahead_states.pop_front(); } if (!SaveMemoryState(&mss)) { Log_ErrorPrint("Failed to save runahead state."); return; } s_runahead_states.push_back(std::move(mss)); } bool System::DoRunahead() { #ifdef PROFILE_MEMORY_SAVE_STATES static Common::Timer replay_timer; #endif if (s_runahead_replay_pending) { #ifdef PROFILE_MEMORY_SAVE_STATES Log_DevPrintf("runahead starting at frame %u", s_frame_number); replay_timer.Reset(); #endif // we need to replay and catch up - load the state, s_runahead_replay_pending = false; if (s_runahead_states.empty() || !LoadMemoryState(s_runahead_states.front())) { s_runahead_states.clear(); return false; } // figure out how many frames we need to run to catch up s_runahead_replay_frames = static_cast(s_runahead_states.size()); // and throw away all the states, forcing us to catch up below s_runahead_states.clear(); // run the frames with no audio SPU::SetAudioOutputMuted(true); #ifdef PROFILE_MEMORY_SAVE_STATES Log_VerbosePrintf("Rewound to frame %u, took %.2f ms", s_frame_number, replay_timer.GetTimeMilliseconds()); #endif // we don't want to save the frame we just loaded. but we are "one frame ahead", because the frame we just tossed // was never saved, so return but don't decrement the counter return true; } else if (s_runahead_replay_frames == 0) { return false; } s_runahead_replay_frames--; if (s_runahead_replay_frames > 0) { // keep running ahead SaveRunaheadState(); return true; } #ifdef PROFILE_MEMORY_SAVE_STATES Log_VerbosePrintf("Running %d frames to catch up took %.2f ms", s_runahead_frames, replay_timer.GetTimeMilliseconds()); #endif // we're all caught up. this frame gets saved in DoMemoryStates(). SPU::SetAudioOutputMuted(false); #ifdef PROFILE_MEMORY_SAVE_STATES Log_DevPrintf("runahead ending at frame %u, took %.2f ms", s_frame_number, replay_timer.GetTimeMilliseconds()); #endif return false; } void System::SetRunaheadReplayFlag() { if (s_runahead_frames == 0 || s_runahead_states.empty()) return; #ifdef PROFILE_MEMORY_SAVE_STATES Log_DevPrintf("Runahead rewind pending..."); #endif s_runahead_replay_pending = true; } void System::ShutdownSystem(bool save_resume_state) { if (!IsValid()) return; if (save_resume_state) SaveResumeState(); if (s_system_executing) s_state = State::Stopping; else DestroySystem(); } bool System::CanUndoLoadState() { return static_cast(m_undo_load_state); } std::optional System::GetUndoSaveStateInfo() { std::optional ssi; if (m_undo_load_state) { m_undo_load_state->SeekAbsolute(0); ssi = InternalGetExtendedSaveStateInfo(m_undo_load_state.get()); m_undo_load_state->SeekAbsolute(0); if (ssi) ssi->timestamp = 0; } return ssi; } bool System::UndoLoadState() { if (!m_undo_load_state) return false; Assert(IsValid()); m_undo_load_state->SeekAbsolute(0); if (!LoadStateFromStream(m_undo_load_state.get(), true)) { Host::ReportErrorAsync("Error", "Failed to load undo state, resetting system."); m_undo_load_state.reset(); ResetSystem(); return false; } Log_InfoPrintf("Loaded undo save state."); m_undo_load_state.reset(); return true; } bool System::SaveUndoLoadState() { if (m_undo_load_state) m_undo_load_state.reset(); m_undo_load_state = ByteStream::CreateGrowableMemoryStream(nullptr, System::MAX_SAVE_STATE_SIZE); if (!SaveStateToStream(m_undo_load_state.get())) { Host::AddOSDMessage(TRANSLATE_STR("OSDMessage", "Failed to save undo load state."), 15.0f); m_undo_load_state.reset(); return false; } Log_InfoPrintf("Saved undo load state: %" PRIu64 " bytes", m_undo_load_state->GetSize()); return true; } bool System::IsRunningAtNonStandardSpeed() { if (!IsValid()) return false; const float target_speed = System::GetTargetSpeed(); return (target_speed <= 0.95f || target_speed >= 1.05f); } s32 System::GetAudioOutputVolume() { return g_settings.GetAudioOutputVolume(IsRunningAtNonStandardSpeed()); } void System::UpdateVolume() { if (!IsValid()) return; SPU::GetOutputStream()->SetOutputVolume(GetAudioOutputVolume()); } bool System::IsDumpingAudio() { return SPU::IsDumpingAudio(); } bool System::StartDumpingAudio(const char* filename) { if (System::IsShutdown()) return false; std::string auto_filename; if (!filename) { const auto& serial = System::GetGameSerial(); if (serial.empty()) { auto_filename = Path::Combine( EmuFolders::Dumps, fmt::format("audio" FS_OSPATH_SEPARATOR_STR "{}.wav", GetTimestampStringForFileName())); } else { auto_filename = Path::Combine(EmuFolders::Dumps, fmt::format("audio" FS_OSPATH_SEPARATOR_STR "{}_{}.wav", serial, GetTimestampStringForFileName())); } filename = auto_filename.c_str(); } if (SPU::StartDumpingAudio(filename)) { Host::AddFormattedOSDMessage(5.0f, TRANSLATE("OSDMessage", "Started dumping audio to '%s'."), filename); return true; } else { Host::AddFormattedOSDMessage(10.0f, TRANSLATE("OSDMessage", "Failed to start dumping audio to '%s'."), filename); return false; } } void System::StopDumpingAudio() { if (System::IsShutdown() || !SPU::StopDumpingAudio()) return; Host::AddOSDMessage(TRANSLATE_STR("OSDMessage", "Stopped dumping audio."), 5.0f); } bool System::SaveScreenshot(const char* filename /* = nullptr */, bool full_resolution /* = true */, bool apply_aspect_ratio /* = true */, bool compress_on_thread /* = true */) { if (!System::IsValid()) return false; std::string auto_filename; if (!filename) { const auto& code = System::GetGameSerial(); const char* extension = "png"; if (code.empty()) { auto_filename = Path::Combine(EmuFolders::Screenshots, fmt::format("{}.{}", GetTimestampStringForFileName(), extension)); } else { auto_filename = Path::Combine(EmuFolders::Screenshots, fmt::format("{}_{}.{}", code, GetTimestampStringForFileName(), extension)); } filename = auto_filename.c_str(); } if (FileSystem::FileExists(filename)) { Host::AddFormattedOSDMessage(10.0f, TRANSLATE("OSDMessage", "Screenshot file '%s' already exists."), filename); return false; } const bool screenshot_saved = g_gpu->RenderScreenshotToFile(filename, g_settings.display_internal_resolution_screenshots, compress_on_thread); if (!screenshot_saved) { Host::AddFormattedOSDMessage(10.0f, TRANSLATE("OSDMessage", "Failed to save screenshot to '%s'"), filename); return false; } Host::AddFormattedOSDMessage(5.0f, TRANSLATE("OSDMessage", "Screenshot saved to '%s'."), filename); return true; } std::string System::GetGameSaveStateFileName(const std::string_view& serial, s32 slot) { if (slot < 0) return Path::Combine(EmuFolders::SaveStates, fmt::format("{}_resume.sav", serial)); else return Path::Combine(EmuFolders::SaveStates, fmt::format("{}_{}.sav", serial, slot)); } std::string System::GetGlobalSaveStateFileName(s32 slot) { if (slot < 0) return Path::Combine(EmuFolders::SaveStates, "resume.sav"); else return Path::Combine(EmuFolders::SaveStates, fmt::format("savestate_{}.sav", slot)); } std::vector System::GetAvailableSaveStates(const char* serial) { std::vector si; std::string path; auto add_path = [&si](std::string path, s32 slot, bool global) { FILESYSTEM_STAT_DATA sd; if (!FileSystem::StatFile(path.c_str(), &sd)) return; si.push_back(SaveStateInfo{std::move(path), sd.ModificationTime, static_cast(slot), global}); }; if (serial && std::strlen(serial) > 0) { add_path(GetGameSaveStateFileName(serial, -1), -1, false); for (s32 i = 1; i <= PER_GAME_SAVE_STATE_SLOTS; i++) add_path(GetGameSaveStateFileName(serial, i), i, false); } for (s32 i = 1; i <= GLOBAL_SAVE_STATE_SLOTS; i++) add_path(GetGlobalSaveStateFileName(i), i, true); return si; } std::optional System::GetSaveStateInfo(const char* serial, s32 slot) { const bool global = (!serial || serial[0] == 0); std::string path = global ? GetGlobalSaveStateFileName(slot) : GetGameSaveStateFileName(serial, slot); FILESYSTEM_STAT_DATA sd; if (!FileSystem::StatFile(path.c_str(), &sd)) return std::nullopt; return SaveStateInfo{std::move(path), sd.ModificationTime, slot, global}; } std::optional System::GetExtendedSaveStateInfo(const char* path) { FILESYSTEM_STAT_DATA sd; if (!FileSystem::StatFile(path, &sd)) return std::nullopt; std::unique_ptr stream = ByteStream::OpenFile(path, BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_SEEKABLE); if (!stream) return std::nullopt; std::optional ssi(InternalGetExtendedSaveStateInfo(stream.get())); if (ssi) ssi->timestamp = sd.ModificationTime; return ssi; } std::optional System::InternalGetExtendedSaveStateInfo(ByteStream* stream) { SAVE_STATE_HEADER header; if (!stream->Read(&header, sizeof(header)) || header.magic != SAVE_STATE_MAGIC) return std::nullopt; ExtendedSaveStateInfo ssi; if (header.version < SAVE_STATE_MINIMUM_VERSION || header.version > SAVE_STATE_VERSION) { ssi.title = fmt::format(TRANSLATE_FS("System", "Invalid version {} ({} version {})"), header.version, header.version > SAVE_STATE_VERSION ? "maximum" : "minimum", header.version > SAVE_STATE_VERSION ? SAVE_STATE_VERSION : SAVE_STATE_MINIMUM_VERSION); return ssi; } header.title[sizeof(header.title) - 1] = 0; ssi.title = header.title; header.serial[sizeof(header.serial) - 1] = 0; ssi.serial = header.serial; if (header.media_filename_length > 0 && (header.offset_to_media_filename + header.media_filename_length) <= stream->GetSize()) { stream->SeekAbsolute(header.offset_to_media_filename); ssi.media_path.resize(header.media_filename_length); if (!stream->Read2(ssi.media_path.data(), header.media_filename_length)) std::string().swap(ssi.media_path); } if (header.screenshot_width > 0 && header.screenshot_height > 0 && header.screenshot_size > 0 && (static_cast(header.offset_to_screenshot) + static_cast(header.screenshot_size)) <= stream->GetSize()) { stream->SeekAbsolute(header.offset_to_screenshot); ssi.screenshot_data.resize((header.screenshot_size + 3u) / 4u); if (stream->Read2(ssi.screenshot_data.data(), header.screenshot_size)) { ssi.screenshot_width = header.screenshot_width; ssi.screenshot_height = header.screenshot_height; } else { decltype(ssi.screenshot_data)().swap(ssi.screenshot_data); } } return ssi; } void System::DeleteSaveStates(const char* serial, bool resume) { const std::vector states(GetAvailableSaveStates(serial)); for (const SaveStateInfo& si : states) { if (si.global || (!resume && si.slot < 0)) continue; Log_InfoPrintf("Removing save state at '%s'", si.path.c_str()); if (!FileSystem::DeleteFile(si.path.c_str())) Log_ErrorPrintf("Failed to delete save state file '%s'", si.path.c_str()); } } std::string System::GetMostRecentResumeSaveStatePath() { std::vector files; if (!FileSystem::FindFiles(EmuFolders::SaveStates.c_str(), "*resume.sav", FILESYSTEM_FIND_FILES, &files) || files.empty()) { return {}; } FILESYSTEM_FIND_DATA* most_recent = &files[0]; for (FILESYSTEM_FIND_DATA& file : files) { if (file.ModificationTime > most_recent->ModificationTime) most_recent = &file; } return std::move(most_recent->FileName); } std::string System::GetCheatFileName() { std::string ret; const std::string& title = System::GetGameTitle(); if (!title.empty()) ret = Path::Combine(EmuFolders::Cheats, fmt::format("{}.cht", title.c_str())); return ret; } bool System::LoadCheatList(const char* filename) { if (System::IsShutdown()) return false; std::unique_ptr cl = std::make_unique(); if (!cl->LoadFromFile(filename, CheatList::Format::Autodetect)) { Host::AddFormattedOSDMessage(15.0f, TRANSLATE("OSDMessage", "Failed to load cheats from '%s'."), filename); return false; } if (cl->GetEnabledCodeCount() > 0) { Host::AddOSDMessage( fmt::format(TRANSLATE_FS("OSDMessage", "{} cheats are enabled. This may result in instability."), cl->GetEnabledCodeCount()), 30.0f); } System::SetCheatList(std::move(cl)); return true; } bool System::LoadCheatListFromGameTitle() { // Called when booting, needs to test for shutdown. if (IsShutdown() || Achievements::IsHardcoreModeActive()) return false; const std::string filename(GetCheatFileName()); if (filename.empty() || !FileSystem::FileExists(filename.c_str())) return false; return LoadCheatList(filename.c_str()); } bool System::LoadCheatListFromDatabase() { if (IsShutdown() || s_running_game_serial.empty() || Achievements::IsHardcoreModeActive()) return false; std::unique_ptr cl = std::make_unique(); if (!cl->LoadFromPackage(s_running_game_serial)) return false; Log_InfoPrintf("Loaded %u cheats from database.", cl->GetCodeCount()); SetCheatList(std::move(cl)); return true; } bool System::SaveCheatList() { if (!System::IsValid() || !System::HasCheatList()) return false; const std::string filename(GetCheatFileName()); if (filename.empty()) return false; if (!System::GetCheatList()->SaveToPCSXRFile(filename.c_str())) { Host::AddFormattedOSDMessage(15.0f, TRANSLATE("OSDMessage", "Failed to save cheat list to '%s'"), filename.c_str()); } return true; } bool System::SaveCheatList(const char* filename) { if (!System::IsValid() || !System::HasCheatList()) return false; if (!System::GetCheatList()->SaveToPCSXRFile(filename)) return false; // This shouldn't be needed, but lupdate doesn't gather this string otherwise... const u32 code_count = System::GetCheatList()->GetCodeCount(); Host::AddOSDMessage(fmt::format(TRANSLATE_FS("OSDMessage", "Saved {} cheats to '{}'."), code_count, filename), 5.0f); return true; } bool System::DeleteCheatList() { if (!System::IsValid()) return false; const std::string filename(GetCheatFileName()); if (!filename.empty()) { if (!FileSystem::DeleteFile(filename.c_str())) return false; Host::AddOSDMessage(fmt::format(TRANSLATE_FS("OSDMessage", "Deleted cheat list '{}'."), filename), 5.0f); } System::SetCheatList(nullptr); return true; } void System::ClearCheatList(bool save_to_file) { if (!System::IsValid()) return; CheatList* cl = System::GetCheatList(); if (!cl) return; while (cl->GetCodeCount() > 0) cl->RemoveCode(cl->GetCodeCount() - 1); if (save_to_file) SaveCheatList(); } void System::SetCheatCodeState(u32 index, bool enabled, bool save_to_file) { if (!System::IsValid() || !System::HasCheatList()) return; CheatList* cl = System::GetCheatList(); if (index >= cl->GetCodeCount()) return; CheatCode& cc = cl->GetCode(index); if (cc.enabled == enabled) return; cc.enabled = enabled; if (!enabled) cc.ApplyOnDisable(); if (enabled) { Host::AddFormattedOSDMessage(5.0f, TRANSLATE("OSDMessage", "Cheat '%s' enabled."), cc.description.c_str()); } else { Host::AddFormattedOSDMessage(5.0f, TRANSLATE("OSDMessage", "Cheat '%s' disabled."), cc.description.c_str()); } if (save_to_file) SaveCheatList(); } void System::ApplyCheatCode(u32 index) { if (!System::HasCheatList() || index >= System::GetCheatList()->GetCodeCount()) return; const CheatCode& cc = System::GetCheatList()->GetCode(index); if (!cc.enabled) { cc.Apply(); Host::AddFormattedOSDMessage(5.0f, TRANSLATE("OSDMessage", "Applied cheat '%s'."), cc.description.c_str()); } else { Host::AddFormattedOSDMessage(5.0f, TRANSLATE("OSDMessage", "Cheat '%s' is already enabled."), cc.description.c_str()); } } void System::ToggleWidescreen() { g_settings.gpu_widescreen_hack = !g_settings.gpu_widescreen_hack; const DisplayAspectRatio user_ratio = Settings::ParseDisplayAspectRatio( Host::GetStringSettingValue("Display", "AspectRatio", Settings::GetDisplayAspectRatioName(Settings::DEFAULT_DISPLAY_ASPECT_RATIO)) .c_str()) .value_or(DisplayAspectRatio::Auto); ; if (user_ratio == DisplayAspectRatio::Auto || user_ratio == DisplayAspectRatio::PAR1_1 || user_ratio == DisplayAspectRatio::R4_3) { g_settings.display_aspect_ratio = g_settings.gpu_widescreen_hack ? DisplayAspectRatio::R16_9 : user_ratio; } else { g_settings.display_aspect_ratio = g_settings.gpu_widescreen_hack ? user_ratio : DisplayAspectRatio::Auto; } if (g_settings.gpu_widescreen_hack) { Host::AddKeyedOSDMessage( "WidescreenHack", fmt::format(TRANSLATE_FS("OSDMessage", "Widescreen hack is now enabled, and aspect ratio is set to {}."), Settings::GetDisplayAspectRatioDisplayName(g_settings.display_aspect_ratio)), 5.0f); } else { Host::AddKeyedOSDMessage( "WidescreenHack", fmt::format(TRANSLATE_FS("OSDMessage", "Widescreen hack is now disabled, and aspect ratio is set to {}."), Settings::GetDisplayAspectRatioDisplayName(g_settings.display_aspect_ratio), 5.0f)); } GTE::UpdateAspectRatio(); } void System::ToggleSoftwareRendering() { if (IsShutdown() || g_settings.gpu_renderer == GPURenderer::Software) return; const GPURenderer new_renderer = g_gpu->IsHardwareRenderer() ? GPURenderer::Software : g_settings.gpu_renderer; Host::AddKeyedFormattedOSDMessage("SoftwareRendering", 5.0f, TRANSLATE("OSDMessage", "Switching to %s renderer..."), Settings::GetRendererDisplayName(new_renderer)); RecreateGPU(new_renderer); ResetPerformanceCounters(); } void System::RequestDisplaySize(float scale /*= 0.0f*/) { if (!IsValid()) return; if (scale == 0.0f) scale = g_gpu->IsHardwareRenderer() ? static_cast(g_settings.gpu_resolution_scale) : 1.0f; const float y_scale = (static_cast(g_gpu->GetDisplayWidth()) / static_cast(g_gpu->GetDisplayHeight())) / g_gpu->GetDisplayAspectRatio(); const u32 requested_width = std::max(static_cast(std::ceil(static_cast(g_gpu->GetDisplayWidth()) * scale)), 1); const u32 requested_height = std::max(static_cast(std::ceil(static_cast(g_gpu->GetDisplayHeight()) * y_scale * scale)), 1); Host::RequestResizeHostDisplay(static_cast(requested_width), static_cast(requested_height)); } void System::HostDisplayResized() { if (!IsValid()) return; if (g_settings.gpu_widescreen_hack && g_settings.display_aspect_ratio == DisplayAspectRatio::MatchWindow) GTE::UpdateAspectRatio(); g_gpu->UpdateResolutionScale(); } bool System::PresentDisplay(bool allow_skip_present) { const bool skip_present = allow_skip_present && g_gpu_device->ShouldSkipDisplayingFrame(); Host::BeginPresentFrame(); // acquire for IO.MousePos. std::atomic_thread_fence(std::memory_order_acquire); if (!skip_present) { FullscreenUI::Render(); ImGuiManager::RenderTextOverlays(); ImGuiManager::RenderOSDMessages(); if (s_state == State::Running) ImGuiManager::RenderSoftwareCursors(); } // Debug windows are always rendered, otherwise mouse input breaks on skip. ImGuiManager::RenderOverlayWindows(); ImGuiManager::RenderDebugWindows(); bool do_present; if (g_gpu && !skip_present) do_present = g_gpu->PresentDisplay(); else do_present = g_gpu_device->BeginPresent(skip_present); if (do_present) { g_gpu_device->RenderImGui(); g_gpu_device->EndPresent(); if (g_gpu_device->IsGPUTimingEnabled()) { s_accumulated_gpu_time += g_gpu_device->GetAndResetAccumulatedGPUTime(); s_presents_since_last_update++; } } else { // Still need to kick ImGui or it gets cranky. ImGui::Render(); } ImGuiManager::NewFrame(); return do_present; } void System::InvalidateDisplay() { PresentDisplay(false); if (g_gpu) g_gpu->RestoreDeviceContext(); } void System::SetTimerResolutionIncreased(bool enabled) { #if defined(_WIN32) static bool current_state = false; if (current_state == enabled) return; current_state = enabled; if (enabled) timeBeginPeriod(1); else timeEndPeriod(1); #endif } void System::UpdateSessionTime(const std::string& prev_serial) { const u64 ctime = Common::Timer::GetCurrentValue(); if (!prev_serial.empty() && GameList::IsGameListLoaded()) { // round up to seconds const std::time_t etime = static_cast(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time))); const std::time_t wtime = std::time(nullptr); GameList::AddPlayedTimeForSerial(prev_serial, wtime, etime); } s_session_start_time = ctime; } u64 System::GetSessionPlayedTime() { const u64 ctime = Common::Timer::GetCurrentValue(); return static_cast(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time))); } #ifdef ENABLE_DISCORD_PRESENCE void System::InitializeDiscordPresence() { if (s_discord_presence_active) return; DiscordEventHandlers handlers = {}; Discord_Initialize("705325712680288296", &handlers, 0, nullptr); s_discord_presence_active = true; UpdateDiscordPresence(); } void System::ShutdownDiscordPresence() { if (!s_discord_presence_active) return; Discord_ClearPresence(); Discord_Shutdown(); s_discord_presence_active = false; } void System::UpdateDiscordPresence() { if (!s_discord_presence_active) return; // https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields DiscordRichPresence rp = {}; rp.largeImageKey = "duckstation_logo"; rp.largeImageText = "DuckStation PS1/PSX Emulator"; rp.startTimestamp = std::time(nullptr); rp.details = "No Game Running"; if (IsValidOrInitializing()) { // Use disc set name if it's not a custom title. if (s_running_game_entry && !s_running_game_entry->disc_set_name.empty() && s_running_game_title == s_running_game_entry->title) { rp.details = s_running_game_entry->disc_set_name.c_str(); } else { rp.details = s_running_game_title.empty() ? "Unknown Game" : s_running_game_title.c_str(); } } std::string state_string; if (Achievements::HasRichPresence()) { const auto lock = Achievements::GetLock(); state_string = StringUtil::Ellipsise(Achievements::GetRichPresenceString(), 128); rp.state = state_string.c_str(); } Discord_UpdatePresence(&rp); } void System::PollDiscordPresence() { if (!s_discord_presence_active) return; Discord_RunCallbacks(); } #endif