diff options
| author | JMC47 <JMC4789@gmail.com> | 2025-12-22 13:29:05 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-22 13:29:05 -0500 |
| commit | cd99d488597b68ae7c2776c504131782e76b92ef (patch) | |
| tree | a1b1323b0fd8a953c487b8991fd4e0d3e099eb7c /Source/Core | |
| parent | 05a204edc6f010224ebab760eb0f9f9a7fe74e51 (diff) | |
| parent | e5ad814142257063ca00847c40561ce3d852f4f1 (diff) | |
Merge pull request #14056 from jordan-woyak/cached-blob-reader
Core/DiscIO: Add a setting to load games into memory.
Diffstat (limited to 'Source/Core')
| -rw-r--r-- | Source/Core/Common/MemArena.h | 8 | ||||
| -rw-r--r-- | Source/Core/Core/Boot/Boot.cpp | 6 | ||||
| -rw-r--r-- | Source/Core/Core/Config/MainSettings.cpp | 1 | ||||
| -rw-r--r-- | Source/Core/Core/Config/MainSettings.h | 1 | ||||
| -rw-r--r-- | Source/Core/Core/HW/DVD/DVDInterface.cpp | 3 | ||||
| -rw-r--r-- | Source/Core/DiscIO/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | Source/Core/DiscIO/CachedBlob.cpp | 239 | ||||
| -rw-r--r-- | Source/Core/DiscIO/CachedBlob.h | 16 | ||||
| -rw-r--r-- | Source/Core/DiscIO/Volume.cpp | 34 | ||||
| -rw-r--r-- | Source/Core/DiscIO/Volume.h | 3 | ||||
| -rw-r--r-- | Source/Core/DolphinLib.props | 2 | ||||
| -rw-r--r-- | Source/Core/DolphinQt/Settings/GeneralPane.cpp | 10 | ||||
| -rw-r--r-- | Source/Core/DolphinQt/Settings/GeneralPane.h | 1 |
13 files changed, 313 insertions, 13 deletions
diff --git a/Source/Core/Common/MemArena.h b/Source/Core/Common/MemArena.h index d9d472079c..dd07f70398 100644 --- a/Source/Core/Common/MemArena.h +++ b/Source/Core/Common/MemArena.h @@ -187,6 +187,14 @@ public: #endif } + void EnsureMemoryPagesWritable(size_t offset, size_t size) + { +#ifdef _WIN32 + for (const auto end_offset = offset + size; offset < end_offset; offset += BLOCK_SIZE) + EnsureMemoryPageWritable(offset); +#endif + } + private: void* m_memory = nullptr; size_t m_size = 0; diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index 755ed0427a..236cea81b9 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -240,7 +240,7 @@ std::unique_ptr<BootParameters> BootParameters::GenerateFromFile(std::vector<std ".elf"}}; if (disc_image_extensions.contains(extension)) { - std::unique_ptr<DiscIO::VolumeDisc> disc = DiscIO::CreateDisc(path); + std::unique_ptr<DiscIO::VolumeDisc> disc = DiscIO::CreateDiscForCore(path); if (disc) { return std::make_unique<BootParameters>(Disc{std::move(path), std::move(disc), paths}, @@ -469,7 +469,7 @@ static void SetDefaultDisc(DVD::DVDInterface& dvd_interface) { const std::string default_iso = Config::Get(Config::MAIN_DEFAULT_ISO); if (!default_iso.empty()) - SetDisc(dvd_interface, DiscIO::CreateDisc(default_iso)); + SetDisc(dvd_interface, DiscIO::CreateDiscForCore(default_iso)); } static void CopyDefaultExceptionHandlers(Core::System& system) @@ -629,7 +629,7 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard, if (ipl.disc) { NOTICE_LOG_FMT(BOOT, "Inserting disc: {}", ipl.disc->path); - SetDisc(system.GetDVDInterface(), DiscIO::CreateDisc(ipl.disc->path), + SetDisc(system.GetDVDInterface(), DiscIO::CreateDiscForCore(ipl.disc->path), ipl.disc->auto_disc_change_paths); } else diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 4e06ec3713..e42796e864 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -58,6 +58,7 @@ constexpr bool DEFAULT_CPU_THREAD = true; constexpr bool DEFAULT_CPU_THREAD = false; #endif const Info<bool> MAIN_CPU_THREAD{{System::Main, "Core", "CPUThread"}, DEFAULT_CPU_THREAD}; +const Info<bool> MAIN_LOAD_GAME_INTO_MEMORY{{System::Main, "Core", "LoadGameIntoMemory"}, false}; const Info<bool> MAIN_SYNC_ON_SKIP_IDLE{{System::Main, "Core", "SyncOnSkipIdle"}, true}; const Info<std::string> MAIN_DEFAULT_ISO{{System::Main, "Core", "DefaultISO"}, ""}; const Info<bool> MAIN_ENABLE_CHEATS{{System::Main, "Core", "EnableCheats"}, false}; diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 86f206d3f4..b6a7094c93 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -68,6 +68,7 @@ extern const Info<bool> MAIN_CORRECT_TIME_DRIFT; extern const Info<bool> MAIN_RUSH_FRAME_PRESENTATION; extern const Info<bool> MAIN_SMOOTH_EARLY_PRESENTATION; extern const Info<bool> MAIN_CPU_THREAD; +extern const Info<bool> MAIN_LOAD_GAME_INTO_MEMORY; extern const Info<bool> MAIN_SYNC_ON_SKIP_IDLE; extern const Info<std::string> MAIN_DEFAULT_ISO; extern const Info<bool> MAIN_ENABLE_CHEATS; diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp index 410f5763a5..49cafccb70 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp @@ -428,7 +428,8 @@ void DVDInterface::EjectDiscCallback(Core::System& system, u64 userdata, s64 cyc void DVDInterface::InsertDiscCallback(Core::System& system, u64 userdata, s64 cyclesLate) { auto& di = system.GetDVDInterface(); - std::unique_ptr<DiscIO::VolumeDisc> new_disc = DiscIO::CreateDisc(di.m_disc_path_to_insert); + std::unique_ptr<DiscIO::VolumeDisc> new_disc = + DiscIO::CreateDiscForCore(di.m_disc_path_to_insert); if (new_disc) di.SetDisc(std::move(new_disc), {}); diff --git a/Source/Core/DiscIO/CMakeLists.txt b/Source/Core/DiscIO/CMakeLists.txt index 884f3de4f2..e2664f4a1a 100644 --- a/Source/Core/DiscIO/CMakeLists.txt +++ b/Source/Core/DiscIO/CMakeLists.txt @@ -3,6 +3,8 @@ add_library(discio Blob.h CISOBlob.cpp CISOBlob.h + CachedBlob.cpp + CachedBlob.h CompressedBlob.cpp CompressedBlob.h DirectoryBlob.cpp diff --git a/Source/Core/DiscIO/CachedBlob.cpp b/Source/Core/DiscIO/CachedBlob.cpp new file mode 100644 index 0000000000..98ed8ba31f --- /dev/null +++ b/Source/Core/DiscIO/CachedBlob.cpp @@ -0,0 +1,239 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DiscIO/CachedBlob.h" + +#include <atomic> +#include <chrono> +#include <thread> + +#include "Common/CommonTypes.h" +#include "Common/Logging/Log.h" +#include "Common/MemArena.h" + +#include "DiscIO/DiscScrubber.h" +#include "DiscIO/Volume.h" + +namespace DiscIO +{ +class CacheFiller final +{ +public: + explicit CacheFiller(std::unique_ptr<BlobReader> reader, bool attempt_to_scrub) + : m_thread{&CacheFiller::ThreadFunc, this, std::move(reader), attempt_to_scrub} + { + } + + ~CacheFiller() + { + m_stop_thread.store(true, std::memory_order_relaxed); + m_thread.join(); + } + + bool Read(u64 offset, u64 size, u8* out_ptr) + { + if (size == 0) + { + // Just return early to safely handle a read of zero if that were to happen. + // This avoids a m_memory_region_data initialization race. + return true; + } + + switch (GetCacheState(offset, size)) + { + case CacheState::Cached: + std::memcpy(out_ptr, m_memory_region_data + offset, size); + return true; + + case CacheState::Scrubbed: + WARN_LOG_FMT(DISCIO, + "CachedBlobReader: Read({}, {}) hits a scrubbed cluster which is not cached.", + offset, size); + return false; + + default: + return false; + } + } + +private: + enum class CacheState + { + Cached, + NotCached, + Scrubbed, + }; + + CacheState GetCacheState(u64 offset, u64 size) + { + const auto cache_pos = m_cache_filled_pos.load(std::memory_order_acquire); + + const auto end_pos = offset + size; + if (end_pos > cache_pos) + return CacheState::NotCached; + + for (u64 i = offset; i < end_pos; i += DiscScrubber::CLUSTER_SIZE) + { + if (m_scrubber.CanBlockBeScrubbed(i)) + return CacheState::Scrubbed; + } + + return CacheState::Cached; + } + + void ThreadFunc(std::unique_ptr<BlobReader> reader, bool attempt_to_scrub) + { + static constexpr auto PERIODIC_LOG_TIME = std::chrono::seconds{1}; + + const auto start_time = Clock::now(); + const u64 total_size = reader->GetDataSize(); + + m_memory_region_data = static_cast<u8*>(m_memory_region.Create(total_size)); + if (m_memory_region_data == nullptr) + { + ERROR_LOG_FMT(DISCIO, "CachedBlobReader: Failed to create memory region."); + return; + } + + // Returns CLUSTER_SIZE or smaller at the end of the file. + const auto get_read_size = [&](u64 pos) { + return std::min<u64>(total_size - pos, DiscScrubber::CLUSTER_SIZE); + }; + + // Used for periodic progress logging. + u64 total_bytes_to_commit = total_size; + + if (attempt_to_scrub) + { + const auto volume = CreateVolume(reader->CopyReader()); + if (volume != nullptr && m_scrubber.SetupScrub(*volume)) + { + for (u64 i = 0; i < total_size; i += DiscScrubber::CLUSTER_SIZE) + { + if (m_scrubber.CanBlockBeScrubbed(i)) + total_bytes_to_commit -= get_read_size(i); + } + } + else + { + WARN_LOG_FMT(DISCIO, "CachedBlobReader: Failed to scrub. The entire file will be cached."); + } + } + + auto next_log_time = start_time + PERIODIC_LOG_TIME; + u64 read_offset = 0; + u64 committed_count = 0; + + while (true) + { + if (m_stop_thread.load(std::memory_order_relaxed)) + { + INFO_LOG_FMT(DISCIO, "CachedBlobReader: Stopped"); + break; + } + + const auto read_size = get_read_size(read_offset); + if (read_size == 0) + { + const auto total_time = DT_s{Clock::now() - start_time}.count(); + + static constexpr auto mib_scale = double(1 << 20); + + NOTICE_LOG_FMT( + DISCIO, "CachedBlobReader: Completed. Cached {:.2f} of {:.2f} MiB in {:.2f} seconds.", + committed_count / mib_scale, total_size / mib_scale, total_time); + break; + } + + if (!m_scrubber.CanBlockBeScrubbed(read_offset)) + { + m_memory_region.EnsureMemoryPagesWritable(read_offset, read_size); + + if (!reader->Read(read_offset, read_size, m_memory_region_data + read_offset)) + { + ERROR_LOG_FMT(DISCIO, "CachedBlobReader: Read({}, {}) failed.", read_offset, read_size); + break; + } + + committed_count += read_size; + } + + read_offset += read_size; + m_cache_filled_pos.store(read_offset, std::memory_order_release); + + if (const auto now = Clock::now(); now >= next_log_time) + { + INFO_LOG_FMT(DISCIO, "CachedBlobReader: Progress: {}%", + committed_count * 100 / total_bytes_to_commit); + next_log_time = now + PERIODIC_LOG_TIME; + } + } + } + + // The thread has read non-scrubbed bytes into memory up to this point. + std::atomic<u64> m_cache_filled_pos{}; + + Common::LazyMemoryRegion m_memory_region; + u8* m_memory_region_data{}; + + DiscScrubber m_scrubber; + + std::atomic_bool m_stop_thread{}; + std::thread m_thread; +}; + +class CachedBlobReader final : public BlobReader +{ +public: + explicit CachedBlobReader(std::unique_ptr<BlobReader> reader, bool attempt_to_scrub) + : m_cache_filler{std::make_shared<CacheFiller>(reader->CopyReader(), attempt_to_scrub)}, + m_reader{std::move(reader)} + + { + INFO_LOG_FMT(DISCIO, "CachedBlobReader: Created"); + } + + CachedBlobReader(std::shared_ptr<CacheFiller> cache_filler, std::unique_ptr<BlobReader> reader) + : m_cache_filler{std::move(cache_filler)}, m_reader{std::move(reader)} + { + INFO_LOG_FMT(DISCIO, "CachedBlobReader: Copied"); + } + + std::unique_ptr<BlobReader> CopyReader() const override + { + return std::make_unique<CachedBlobReader>(m_cache_filler, m_reader->CopyReader()); + } + + BlobType GetBlobType() const override { return m_reader->GetBlobType(); } + u64 GetRawSize() const override { return GetDataSize(); } + u64 GetDataSize() const override { return m_reader->GetDataSize(); } + DataSizeType GetDataSizeType() const override { return m_reader->GetDataSizeType(); } + + u64 GetBlockSize() const override { return 0; } + bool HasFastRandomAccessInBlock() const override { return true; } + std::string GetCompressionMethod() const override { return {}; } + std::optional<int> GetCompressionLevel() const override { return std::nullopt; } + + bool Read(u64 offset, u64 size, u8* out_ptr) override + { + return m_cache_filler->Read(offset, size, out_ptr) || m_reader->Read(offset, size, out_ptr); + } + +private: + // A shared object does the cache filling for sensible CopyReader behavior. + const std::shared_ptr<CacheFiller> m_cache_filler; + + const std::unique_ptr<BlobReader> m_reader; +}; + +std::unique_ptr<BlobReader> CreateCachedBlobReader(std::unique_ptr<BlobReader> reader) +{ + return std::make_unique<CachedBlobReader>(std::move(reader), false); +} + +std::unique_ptr<BlobReader> CreateScrubbingCachedBlobReader(std::unique_ptr<BlobReader> reader) +{ + return std::make_unique<CachedBlobReader>(std::move(reader), true); +} + +} // namespace DiscIO diff --git a/Source/Core/DiscIO/CachedBlob.h b/Source/Core/DiscIO/CachedBlob.h new file mode 100644 index 0000000000..f99ad552bb --- /dev/null +++ b/Source/Core/DiscIO/CachedBlob.h @@ -0,0 +1,16 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <memory> + +#include "DiscIO/Blob.h" + +namespace DiscIO +{ + +std::unique_ptr<BlobReader> CreateCachedBlobReader(std::unique_ptr<BlobReader> reader); +std::unique_ptr<BlobReader> CreateScrubbingCachedBlobReader(std::unique_ptr<BlobReader> reader); + +} // namespace DiscIO diff --git a/Source/Core/DiscIO/Volume.cpp b/Source/Core/DiscIO/Volume.cpp index b6d84254f0..7e5dcc7976 100644 --- a/Source/Core/DiscIO/Volume.cpp +++ b/Source/Core/DiscIO/Volume.cpp @@ -3,7 +3,7 @@ #include "DiscIO/Volume.h" -#include <algorithm> +#include <functional> #include <map> #include <memory> #include <optional> @@ -16,8 +16,11 @@ #include "Common/Crypto/SHA1.h" #include "Common/StringUtil.h" +#include "Core/Config/MainSettings.h" #include "Core/IOS/ES/Formats.h" + #include "DiscIO/Blob.h" +#include "DiscIO/CachedBlob.h" #include "DiscIO/DiscUtils.h" #include "DiscIO/Enums.h" #include "DiscIO/VolumeDisc.h" @@ -84,16 +87,21 @@ std::map<Language, std::string> Volume::ReadWiiNames(const std::vector<char16_t> return names; } -static std::unique_ptr<VolumeDisc> TryCreateDisc(std::unique_ptr<BlobReader>& reader) +template <typename T = std::identity> +static std::unique_ptr<VolumeDisc> TryCreateDisc(std::unique_ptr<BlobReader> reader, + const T& reader_adapter_factory = {}) { if (!reader) return nullptr; + // `reader_adapter_factory` is used *after* successful magic word read. + // This prevents `CachedBlobReader` from showing warnings when failing to scrub a .dol file. + if (reader->ReadSwapped<u32>(0x18) == WII_DISC_MAGIC) - return std::make_unique<VolumeWii>(std::move(reader)); + return std::make_unique<VolumeWii>(reader_adapter_factory(std::move(reader))); if (reader->ReadSwapped<u32>(0x1C) == GAMECUBE_DISC_MAGIC) - return std::make_unique<VolumeGC>(std::move(reader)); + return std::make_unique<VolumeGC>(reader_adapter_factory(std::move(reader))); // No known magic words found return nullptr; @@ -101,7 +109,7 @@ static std::unique_ptr<VolumeDisc> TryCreateDisc(std::unique_ptr<BlobReader>& re std::unique_ptr<VolumeDisc> CreateDisc(std::unique_ptr<BlobReader> reader) { - return TryCreateDisc(reader); + return TryCreateDisc(std::move(reader)); } std::unique_ptr<VolumeDisc> CreateDisc(const std::string& path) @@ -109,7 +117,15 @@ std::unique_ptr<VolumeDisc> CreateDisc(const std::string& path) return CreateDisc(CreateBlobReader(path)); } -static std::unique_ptr<VolumeWAD> TryCreateWAD(std::unique_ptr<BlobReader>& reader) +std::unique_ptr<VolumeDisc> CreateDiscForCore(const std::string& path) +{ + if (Config::Get(Config::MAIN_LOAD_GAME_INTO_MEMORY)) + return TryCreateDisc(CreateBlobReader(path), CreateScrubbingCachedBlobReader); + + return CreateDisc(path); +} + +static std::unique_ptr<VolumeWAD> TryCreateWAD(std::unique_ptr<BlobReader> reader) { if (!reader) return nullptr; @@ -126,7 +142,7 @@ static std::unique_ptr<VolumeWAD> TryCreateWAD(std::unique_ptr<BlobReader>& read std::unique_ptr<VolumeWAD> CreateWAD(std::unique_ptr<BlobReader> reader) { - return TryCreateWAD(reader); + return TryCreateWAD(std::move(reader)); } std::unique_ptr<VolumeWAD> CreateWAD(const std::string& path) @@ -136,11 +152,11 @@ std::unique_ptr<VolumeWAD> CreateWAD(const std::string& path) std::unique_ptr<Volume> CreateVolume(std::unique_ptr<BlobReader> reader) { - std::unique_ptr<VolumeDisc> disc = TryCreateDisc(reader); + std::unique_ptr<VolumeDisc> disc = TryCreateDisc(std::move(reader)); if (disc) return disc; - std::unique_ptr<VolumeWAD> wad = TryCreateWAD(reader); + std::unique_ptr<VolumeWAD> wad = TryCreateWAD(std::move(reader)); if (wad) return wad; diff --git a/Source/Core/DiscIO/Volume.h b/Source/Core/DiscIO/Volume.h index a7273b0f0e..3f80cd2e3d 100644 --- a/Source/Core/DiscIO/Volume.h +++ b/Source/Core/DiscIO/Volume.h @@ -176,6 +176,9 @@ protected: std::unique_ptr<VolumeDisc> CreateDisc(std::unique_ptr<BlobReader> reader); std::unique_ptr<VolumeDisc> CreateDisc(const std::string& path); +// This version enables caching when the "Load Games into Memory" setting is enabled. +std::unique_ptr<VolumeDisc> CreateDiscForCore(const std::string& path); + std::unique_ptr<VolumeWAD> CreateWAD(std::unique_ptr<BlobReader> reader); std::unique_ptr<VolumeWAD> CreateWAD(const std::string& path); std::unique_ptr<Volume> CreateVolume(std::unique_ptr<BlobReader> reader); diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 5e81cea3a6..37860aa734 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -485,6 +485,7 @@ <ClInclude Include="Core\WiiUtils.h" /> <ClInclude Include="DiscIO\Blob.h" /> <ClInclude Include="DiscIO\CISOBlob.h" /> + <ClInclude Include="DiscIO\CachedBlob.h" /> <ClInclude Include="DiscIO\CompressedBlob.h" /> <ClInclude Include="DiscIO\DirectoryBlob.h" /> <ClInclude Include="DiscIO\DiscExtractor.h" /> @@ -1170,6 +1171,7 @@ <ClCompile Include="Core\WC24PatchEngine.cpp" /> <ClCompile Include="DiscIO\Blob.cpp" /> <ClCompile Include="DiscIO\CISOBlob.cpp" /> + <ClCompile Include="DiscIO\CachedBlob.cpp" /> <ClCompile Include="DiscIO\CompressedBlob.cpp" /> <ClCompile Include="DiscIO\DirectoryBlob.cpp" /> <ClCompile Include="DiscIO\DiscExtractor.cpp" /> diff --git a/Source/Core/DolphinQt/Settings/GeneralPane.cpp b/Source/Core/DolphinQt/Settings/GeneralPane.cpp index 93e3d0d9c8..661f5f13b2 100644 --- a/Source/Core/DolphinQt/Settings/GeneralPane.cpp +++ b/Source/Core/DolphinQt/Settings/GeneralPane.cpp @@ -90,6 +90,7 @@ void GeneralPane::OnEmulationStateChanged(Core::State state) m_checkbox_dualcore->setEnabled(!running); m_checkbox_cheats->setEnabled(!running); + m_checkbox_load_games_into_memory->setEnabled(!running); m_checkbox_override_region_settings->setEnabled(!running); #ifdef USE_DISCORD_PRESENCE m_checkbox_discord_presence->setEnabled(!running); @@ -147,6 +148,15 @@ void GeneralPane::CreateBasic() m_checkbox_cheats = new ConfigBool(tr("Enable Cheats"), Config::MAIN_ENABLE_CHEATS); basic_group_layout->addWidget(m_checkbox_cheats); + m_checkbox_load_games_into_memory = + new ConfigBool(tr("Load Whole Game Into Memory"), Config::MAIN_LOAD_GAME_INTO_MEMORY); + basic_group_layout->addWidget(m_checkbox_load_games_into_memory); + m_checkbox_load_games_into_memory->SetDescription( + tr("Loads the running game into memory in the background." + "<br><br>This may improve performance with slow or high-latency storage." + "<br>System memory requirements will be much higher with this setting enabled." + "<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>")); + m_checkbox_override_region_settings = new ConfigBool(tr("Allow Mismatched Region Settings"), Config::MAIN_OVERRIDE_REGION_SETTINGS); basic_group_layout->addWidget(m_checkbox_override_region_settings); diff --git a/Source/Core/DolphinQt/Settings/GeneralPane.h b/Source/Core/DolphinQt/Settings/GeneralPane.h index 17d80f7c38..b482172b09 100644 --- a/Source/Core/DolphinQt/Settings/GeneralPane.h +++ b/Source/Core/DolphinQt/Settings/GeneralPane.h @@ -48,6 +48,7 @@ private: ToolTipComboBox* m_combobox_fallback_region; ConfigBool* m_checkbox_dualcore; ConfigBool* m_checkbox_cheats; + ConfigBool* m_checkbox_load_games_into_memory; ConfigBool* m_checkbox_override_region_settings; ConfigBool* m_checkbox_auto_disc_change; #ifdef USE_DISCORD_PRESENCE |
