mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-04-28 21:08:04 +03:00
Merge 8b937187bb
into 8ee64a84c7
This commit is contained in:
commit
d1e270350e
10 changed files with 297 additions and 145 deletions
|
@ -70,6 +70,7 @@ add_library(common
|
||||||
Flag.h
|
Flag.h
|
||||||
FloatUtils.cpp
|
FloatUtils.cpp
|
||||||
FloatUtils.h
|
FloatUtils.h
|
||||||
|
FlushThread.h
|
||||||
FormatUtil.h
|
FormatUtil.h
|
||||||
FPURoundMode.h
|
FPURoundMode.h
|
||||||
GekkoDisassembler.cpp
|
GekkoDisassembler.cpp
|
||||||
|
|
143
Source/Core/Common/FlushThread.h
Normal file
143
Source/Core/Common/FlushThread.h
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
// Copyright 2025 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <semaphore>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
#include "Common/Event.h"
|
||||||
|
#include "Common/Thread.h"
|
||||||
|
|
||||||
|
// This class allows flushing data writes in a delayed manner.
|
||||||
|
// When SetDirty is called the provided function will be invoked on thread with configured delay.
|
||||||
|
// Multiple SetDirty calls may produce just one flush, delay based on the last call.
|
||||||
|
|
||||||
|
namespace Common
|
||||||
|
{
|
||||||
|
|
||||||
|
class FlushThread final
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FlushThread() = default;
|
||||||
|
explicit FlushThread(std::string name, std::function<void()> func)
|
||||||
|
{
|
||||||
|
Reset(std::move(name), std::move(func));
|
||||||
|
}
|
||||||
|
|
||||||
|
~FlushThread() { Shutdown(); }
|
||||||
|
|
||||||
|
FlushThread(const FlushThread&) = delete;
|
||||||
|
FlushThread& operator=(const FlushThread&) = delete;
|
||||||
|
|
||||||
|
FlushThread(FlushThread&&) = delete;
|
||||||
|
FlushThread& operator=(FlushThread&&) = delete;
|
||||||
|
|
||||||
|
// May not take effect until clean.
|
||||||
|
void SetFlushDelay(DT delay) { m_flush_delay.store(delay, std::memory_order_relaxed); }
|
||||||
|
|
||||||
|
// (Re)Starts the thread with the provided flush function.
|
||||||
|
// Other state is unchanged.
|
||||||
|
void Reset(std::string name, std::function<void()> func)
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
|
||||||
|
m_want_shutdown.store(false, std::memory_order_relaxed);
|
||||||
|
m_thread = std::thread{std::bind_front(&FlushThread::ThreadFunc, this), std::move(name),
|
||||||
|
std::move(func)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graceful immediate shutdown. Waits for final flush if necessary.
|
||||||
|
// Does nothing if thread isn't running.
|
||||||
|
void Shutdown()
|
||||||
|
{
|
||||||
|
if (!m_thread.joinable())
|
||||||
|
return;
|
||||||
|
|
||||||
|
WaitForCompletion();
|
||||||
|
m_want_shutdown.store(true, std::memory_order_relaxed);
|
||||||
|
m_event.Set();
|
||||||
|
m_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetDirty()
|
||||||
|
{
|
||||||
|
m_dirty_count.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
m_flush_deadline.store((Clock::now() + m_flush_delay.load(std::memory_order_relaxed)));
|
||||||
|
m_event.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lets the worker immediately flush if necessary.
|
||||||
|
// Does nothing if thread isn't running.
|
||||||
|
void WaitForCompletion()
|
||||||
|
{
|
||||||
|
if (!m_thread.joinable())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_run_freely.release();
|
||||||
|
|
||||||
|
m_event.Set();
|
||||||
|
// Wait for m_dirty_count == 0.
|
||||||
|
while (auto old_count = m_dirty_count.load(std::memory_order_acquire))
|
||||||
|
m_dirty_count.wait(old_count, std::memory_order_acquire);
|
||||||
|
|
||||||
|
m_run_freely.acquire();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
auto GetDeadline() const { return m_flush_deadline.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
|
void WaitUntilFlushIsWanted()
|
||||||
|
{
|
||||||
|
while (!m_run_freely.try_acquire_until(GetDeadline()))
|
||||||
|
{
|
||||||
|
if (Clock::now() >= GetDeadline())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_run_freely.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadFunc(const std::string& name, const std::function<void()>& flush_func)
|
||||||
|
{
|
||||||
|
Common::SetCurrentThreadName(name.c_str());
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
m_event.Wait();
|
||||||
|
|
||||||
|
if (m_want_shutdown.load(std::memory_order_relaxed))
|
||||||
|
break;
|
||||||
|
|
||||||
|
WaitUntilFlushIsWanted();
|
||||||
|
|
||||||
|
const auto cleaning_count = m_dirty_count.load(std::memory_order_relaxed);
|
||||||
|
if (cleaning_count != 0)
|
||||||
|
{
|
||||||
|
flush_func();
|
||||||
|
m_dirty_count.fetch_sub(cleaning_count, std::memory_order_release);
|
||||||
|
m_dirty_count.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incremented when a flush needs to happen.
|
||||||
|
// Decremented by worker-thread to signal completion.
|
||||||
|
std::atomic<u32> m_dirty_count{};
|
||||||
|
|
||||||
|
std::atomic<DT> m_flush_delay{};
|
||||||
|
std::atomic<TimePoint> m_flush_deadline{};
|
||||||
|
|
||||||
|
// Worker tries to acquire this for the flush delay.
|
||||||
|
// Releasing it lets the worker run without waiting.
|
||||||
|
std::counting_semaphore<> m_run_freely{0};
|
||||||
|
|
||||||
|
std::thread m_thread;
|
||||||
|
Common::Event m_event;
|
||||||
|
|
||||||
|
std::atomic_bool m_want_shutdown{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Common
|
|
@ -4,9 +4,7 @@
|
||||||
#include "Core/HW/GCMemcard/GCMemcardDirectory.h"
|
#include "Core/HW/GCMemcard/GCMemcardDirectory.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
@ -25,8 +23,6 @@
|
||||||
#include "Common/Logging/Log.h"
|
#include "Common/Logging/Log.h"
|
||||||
#include "Common/MsgHandler.h"
|
#include "Common/MsgHandler.h"
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
#include "Common/Thread.h"
|
|
||||||
#include "Common/Timer.h"
|
|
||||||
|
|
||||||
#include "Core/Config/MainSettings.h"
|
#include "Core/Config/MainSettings.h"
|
||||||
#include "Core/Config/SessionSettings.h"
|
#include "Core/Config/SessionSettings.h"
|
||||||
|
@ -181,8 +177,7 @@ std::vector<std::string> GCMemcardDirectory::GetFileNamesForGameID(const std::st
|
||||||
GCMemcardDirectory::GCMemcardDirectory(const std::string& directory, ExpansionInterface::Slot slot,
|
GCMemcardDirectory::GCMemcardDirectory(const std::string& directory, ExpansionInterface::Slot slot,
|
||||||
const Memcard::HeaderData& header_data, u32 game_id)
|
const Memcard::HeaderData& header_data, u32 game_id)
|
||||||
: MemoryCardBase(slot, header_data.m_size_mb), m_game_id(game_id), m_last_block(-1),
|
: MemoryCardBase(slot, header_data.m_size_mb), m_game_id(game_id), m_last_block(-1),
|
||||||
m_hdr(header_data), m_bat1(header_data.m_size_mb), m_saves(0), m_save_directory(directory),
|
m_hdr(header_data), m_bat1(header_data.m_size_mb), m_saves(0), m_save_directory(directory)
|
||||||
m_exiting(false)
|
|
||||||
{
|
{
|
||||||
// Use existing header data if available
|
// Use existing header data if available
|
||||||
{
|
{
|
||||||
|
@ -256,44 +251,19 @@ GCMemcardDirectory::GCMemcardDirectory(const std::string& directory, ExpansionIn
|
||||||
m_dir2 = m_dir1;
|
m_dir2 = m_dir1;
|
||||||
m_bat2 = m_bat1;
|
m_bat2 = m_bat1;
|
||||||
|
|
||||||
m_flush_thread = std::thread(&GCMemcardDirectory::FlushThread, this);
|
if (Config::Get(Config::SESSION_SAVE_DATA_WRITABLE))
|
||||||
}
|
|
||||||
|
|
||||||
void GCMemcardDirectory::FlushThread()
|
|
||||||
{
|
|
||||||
if (!Config::Get(Config::SESSION_SAVE_DATA_WRITABLE))
|
|
||||||
{
|
{
|
||||||
return;
|
m_flush_thread.Reset(fmt::format("Memcard {} flushing thread", m_card_slot),
|
||||||
}
|
std::bind_front(&GCMemcardDirectory::FlushToFile, this));
|
||||||
|
|
||||||
Common::SetCurrentThreadName(fmt::format("Memcard {} flushing thread", m_card_slot).c_str());
|
m_flush_thread.SetFlushDelay(std::chrono::seconds{1});
|
||||||
|
|
||||||
constexpr std::chrono::seconds flush_interval{1};
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
// no-op until signalled
|
|
||||||
m_flush_trigger.Wait();
|
|
||||||
|
|
||||||
if (m_exiting.TestAndClear())
|
|
||||||
return;
|
|
||||||
// no-op as long as signalled within flush_interval
|
|
||||||
while (m_flush_trigger.WaitFor(flush_interval))
|
|
||||||
{
|
|
||||||
if (m_exiting.TestAndClear())
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FlushToFile();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GCMemcardDirectory::~GCMemcardDirectory()
|
GCMemcardDirectory::~GCMemcardDirectory()
|
||||||
{
|
{
|
||||||
m_exiting.Set();
|
// Trigger one more flush on Shutdown since Write doesn't always SetDirty.
|
||||||
m_flush_trigger.Set();
|
m_flush_thread.SetDirty();
|
||||||
m_flush_thread.join();
|
|
||||||
|
|
||||||
FlushToFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s32 GCMemcardDirectory::Read(u32 src_address, s32 length, u8* dest_address)
|
s32 GCMemcardDirectory::Read(u32 src_address, s32 length, u8* dest_address)
|
||||||
|
@ -417,7 +387,7 @@ s32 GCMemcardDirectory::Write(u32 dest_address, s32 length, const u8* src_addres
|
||||||
if (extra)
|
if (extra)
|
||||||
extra = Write(dest_address + length, extra, src_address + length);
|
extra = Write(dest_address + length, extra, src_address + length);
|
||||||
if (offset + length == Memcard::BLOCK_SIZE)
|
if (offset + length == Memcard::BLOCK_SIZE)
|
||||||
m_flush_trigger.Set();
|
m_flush_thread.SetDirty();
|
||||||
return length + extra;
|
return length + extra;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -620,7 +590,7 @@ bool GCMemcardDirectory::SetUsedBlocks(int save_index)
|
||||||
|
|
||||||
void GCMemcardDirectory::FlushToFile()
|
void GCMemcardDirectory::FlushToFile()
|
||||||
{
|
{
|
||||||
std::unique_lock l(m_write_mutex);
|
std::lock_guard lk{m_write_mutex};
|
||||||
Memcard::DEntry invalid;
|
Memcard::DEntry invalid;
|
||||||
for (Memcard::GCIFile& save : m_saves)
|
for (Memcard::GCIFile& save : m_saves)
|
||||||
{
|
{
|
||||||
|
@ -719,7 +689,7 @@ void GCMemcardDirectory::FlushToFile()
|
||||||
|
|
||||||
void GCMemcardDirectory::DoState(PointerWrap& p)
|
void GCMemcardDirectory::DoState(PointerWrap& p)
|
||||||
{
|
{
|
||||||
std::unique_lock l(m_write_mutex);
|
std::lock_guard lk{m_write_mutex};
|
||||||
m_last_block = -1;
|
m_last_block = -1;
|
||||||
m_last_block_address = nullptr;
|
m_last_block_address = nullptr;
|
||||||
p.Do(m_save_directory);
|
p.Do(m_save_directory);
|
||||||
|
@ -735,10 +705,10 @@ void MigrateFromMemcardFile(const std::string& directory_name, ExpansionInterfac
|
||||||
DiscIO::Region region)
|
DiscIO::Region region)
|
||||||
{
|
{
|
||||||
File::CreateFullPath(directory_name);
|
File::CreateFullPath(directory_name);
|
||||||
const std::string ini_memcard = Config::GetMemcardPath(card_slot, region);
|
std::string ini_memcard = Config::GetMemcardPath(card_slot, region);
|
||||||
if (File::Exists(ini_memcard))
|
if (File::Exists(ini_memcard))
|
||||||
{
|
{
|
||||||
auto [error_code, memcard] = Memcard::GCMemcard::Open(ini_memcard.c_str());
|
auto [error_code, memcard] = Memcard::GCMemcard::Open(std::move(ini_memcard));
|
||||||
if (!error_code.HasCriticalErrors() && memcard && memcard->IsValid())
|
if (!error_code.HasCriticalErrors() && memcard && memcard->IsValid())
|
||||||
{
|
{
|
||||||
for (u8 i = 0; i < Memcard::DIRLEN; i++)
|
for (u8 i = 0; i < Memcard::DIRLEN; i++)
|
||||||
|
|
|
@ -5,10 +5,9 @@
|
||||||
|
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Common/Event.h"
|
#include "Common/FlushThread.h"
|
||||||
#include "Core/HW/GCMemcard/GCIFile.h"
|
#include "Core/HW/GCMemcard/GCIFile.h"
|
||||||
#include "Core/HW/GCMemcard/GCMemcard.h"
|
#include "Core/HW/GCMemcard/GCMemcard.h"
|
||||||
#include "Core/HW/GCMemcard/GCMemcardBase.h"
|
#include "Core/HW/GCMemcard/GCMemcardBase.h"
|
||||||
|
@ -19,12 +18,12 @@
|
||||||
void MigrateFromMemcardFile(const std::string& directory_name, ExpansionInterface::Slot card_slot,
|
void MigrateFromMemcardFile(const std::string& directory_name, ExpansionInterface::Slot card_slot,
|
||||||
DiscIO::Region region);
|
DiscIO::Region region);
|
||||||
|
|
||||||
class GCMemcardDirectory : public MemoryCardBase
|
class GCMemcardDirectory final : public MemoryCardBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
GCMemcardDirectory(const std::string& directory, ExpansionInterface::Slot slot,
|
GCMemcardDirectory(const std::string& directory, ExpansionInterface::Slot slot,
|
||||||
const Memcard::HeaderData& header_data, u32 game_id);
|
const Memcard::HeaderData& header_data, u32 game_id);
|
||||||
~GCMemcardDirectory();
|
~GCMemcardDirectory() override;
|
||||||
|
|
||||||
GCMemcardDirectory(const GCMemcardDirectory&) = delete;
|
GCMemcardDirectory(const GCMemcardDirectory&) = delete;
|
||||||
GCMemcardDirectory& operator=(const GCMemcardDirectory&) = delete;
|
GCMemcardDirectory& operator=(const GCMemcardDirectory&) = delete;
|
||||||
|
@ -33,8 +32,6 @@ public:
|
||||||
|
|
||||||
static std::vector<std::string> GetFileNamesForGameID(const std::string& directory,
|
static std::vector<std::string> GetFileNamesForGameID(const std::string& directory,
|
||||||
const std::string& game_id);
|
const std::string& game_id);
|
||||||
void FlushToFile();
|
|
||||||
void FlushThread();
|
|
||||||
s32 Read(u32 src_address, s32 length, u8* dest_address) override;
|
s32 Read(u32 src_address, s32 length, u8* dest_address) override;
|
||||||
s32 Write(u32 dest_address, s32 length, const u8* src_address) override;
|
s32 Write(u32 dest_address, s32 length, const u8* src_address) override;
|
||||||
void ClearBlock(u32 address) override;
|
void ClearBlock(u32 address) override;
|
||||||
|
@ -42,6 +39,7 @@ public:
|
||||||
void DoState(PointerWrap& p) override;
|
void DoState(PointerWrap& p) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void FlushToFile();
|
||||||
bool LoadGCI(Memcard::GCIFile gci);
|
bool LoadGCI(Memcard::GCIFile gci);
|
||||||
inline s32 SaveAreaRW(u32 block, bool writing = false);
|
inline s32 SaveAreaRW(u32 block, bool writing = false);
|
||||||
// s32 DirectoryRead(u32 offset, u32 length, u8* dest_address);
|
// s32 DirectoryRead(u32 offset, u32 length, u8* dest_address);
|
||||||
|
@ -61,8 +59,6 @@ private:
|
||||||
std::vector<Memcard::GCIFile> m_saves;
|
std::vector<Memcard::GCIFile> m_saves;
|
||||||
|
|
||||||
std::string m_save_directory;
|
std::string m_save_directory;
|
||||||
Common::Event m_flush_trigger;
|
|
||||||
std::mutex m_write_mutex;
|
std::mutex m_write_mutex;
|
||||||
Common::Flag m_exiting;
|
Common::FlushThread m_flush_thread;
|
||||||
std::thread m_flush_thread;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,19 +8,16 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
|
|
||||||
#include "Common/ChunkFile.h"
|
#include "Common/ChunkFile.h"
|
||||||
#include "Common/CommonPaths.h"
|
|
||||||
#include "Common/CommonTypes.h"
|
#include "Common/CommonTypes.h"
|
||||||
#include "Common/FileUtil.h"
|
#include "Common/FileUtil.h"
|
||||||
#include "Common/IOFile.h"
|
#include "Common/IOFile.h"
|
||||||
#include "Common/Logging/Log.h"
|
#include "Common/Logging/Log.h"
|
||||||
#include "Common/MsgHandler.h"
|
#include "Common/MsgHandler.h"
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
#include "Common/Thread.h"
|
|
||||||
#include "Common/Timer.h"
|
#include "Common/Timer.h"
|
||||||
|
|
||||||
#include "Core/Config/SessionSettings.h"
|
#include "Core/Config/SessionSettings.h"
|
||||||
|
@ -35,11 +32,10 @@
|
||||||
#define SIZE_TO_Mb (1024 * 8 * 16)
|
#define SIZE_TO_Mb (1024 * 8 * 16)
|
||||||
#define MC_HDR_SIZE 0xA000
|
#define MC_HDR_SIZE 0xA000
|
||||||
|
|
||||||
MemoryCard::MemoryCard(const std::string& filename, ExpansionInterface::Slot card_slot,
|
MemoryCard::MemoryCard(std::string filename, ExpansionInterface::Slot card_slot, u16 size_mbits)
|
||||||
u16 size_mbits)
|
: MemoryCardBase(card_slot, size_mbits)
|
||||||
: MemoryCardBase(card_slot, size_mbits), m_filename(filename)
|
|
||||||
{
|
{
|
||||||
File::IOFile file(m_filename, "rb");
|
File::IOFile file(filename, "rb");
|
||||||
if (file)
|
if (file)
|
||||||
{
|
{
|
||||||
// Measure size of the existing memcard file.
|
// Measure size of the existing memcard file.
|
||||||
|
@ -48,7 +44,7 @@ MemoryCard::MemoryCard(const std::string& filename, ExpansionInterface::Slot car
|
||||||
m_memcard_data = std::make_unique<u8[]>(m_memory_card_size);
|
m_memcard_data = std::make_unique<u8[]>(m_memory_card_size);
|
||||||
memset(&m_memcard_data[0], 0xFF, m_memory_card_size);
|
memset(&m_memcard_data[0], 0xFF, m_memory_card_size);
|
||||||
|
|
||||||
INFO_LOG_FMT(EXPANSIONINTERFACE, "Reading memory card {}", m_filename);
|
INFO_LOG_FMT(EXPANSIONINTERFACE, "Reading memory card {}", filename);
|
||||||
file.ReadBytes(&m_memcard_data[0], m_memory_card_size);
|
file.ReadBytes(&m_memcard_data[0], m_memory_card_size);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -62,7 +58,7 @@ MemoryCard::MemoryCard(const std::string& filename, ExpansionInterface::Slot car
|
||||||
// Fills in the first 5 blocks (MC_HDR_SIZE bytes)
|
// Fills in the first 5 blocks (MC_HDR_SIZE bytes)
|
||||||
auto& sram = Core::System::GetInstance().GetSRAM();
|
auto& sram = Core::System::GetInstance().GetSRAM();
|
||||||
const CardFlashId& flash_id = sram.settings_ex.flash_id[Memcard::SLOT_A];
|
const CardFlashId& flash_id = sram.settings_ex.flash_id[Memcard::SLOT_A];
|
||||||
const bool shift_jis = m_filename.find(".JAP.raw") != std::string::npos;
|
const bool shift_jis = filename.find(".JAP.raw") != std::string::npos;
|
||||||
const u32 rtc_bias = sram.settings.rtc_bias;
|
const u32 rtc_bias = sram.settings.rtc_bias;
|
||||||
const u32 sram_language = static_cast<u32>(sram.settings.language);
|
const u32 sram_language = static_cast<u32>(sram.settings.language);
|
||||||
const u64 format_time =
|
const u64 format_time =
|
||||||
|
@ -76,95 +72,62 @@ MemoryCard::MemoryCard(const std::string& filename, ExpansionInterface::Slot car
|
||||||
INFO_LOG_FMT(EXPANSIONINTERFACE, "No memory card found. A new one was created instead.");
|
INFO_LOG_FMT(EXPANSIONINTERFACE, "No memory card found. A new one was created instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Class members (including inherited ones) have now been initialized, so
|
|
||||||
// it's safe to startup the flush thread (which reads them).
|
|
||||||
m_flush_buffer = std::make_unique<u8[]>(m_memory_card_size);
|
m_flush_buffer = std::make_unique<u8[]>(m_memory_card_size);
|
||||||
m_flush_thread = std::thread(&MemoryCard::FlushThread, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
MemoryCard::~MemoryCard()
|
if (Config::Get(Config::SESSION_SAVE_DATA_WRITABLE))
|
||||||
{
|
|
||||||
if (m_flush_thread.joinable())
|
|
||||||
{
|
{
|
||||||
m_flush_trigger.Set();
|
m_flush_thread.Reset(fmt::format("Memcard {} flushing thread", m_card_slot),
|
||||||
|
std::bind_front(&MemoryCard::FlushToFile, this, std::move(filename)));
|
||||||
|
|
||||||
m_flush_thread.join();
|
m_flush_thread.SetFlushDelay(std::chrono::seconds{1});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MemoryCard::FlushThread()
|
MemoryCard::~MemoryCard() = default;
|
||||||
|
|
||||||
|
void MemoryCard::FlushToFile(const std::string& filename)
|
||||||
{
|
{
|
||||||
if (!Config::Get(Config::SESSION_SAVE_DATA_WRITABLE))
|
File::IOFile file(filename, "r+b");
|
||||||
|
|
||||||
|
if (!file)
|
||||||
{
|
{
|
||||||
|
std::string dir;
|
||||||
|
SplitPath(filename, &dir, nullptr, nullptr);
|
||||||
|
if (!File::IsDirectory(dir))
|
||||||
|
{
|
||||||
|
File::CreateFullPath(dir);
|
||||||
|
}
|
||||||
|
file.Open(filename, "wb");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note - file may have changed above, after ctor
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
PanicAlertFmtT(
|
||||||
|
"Could not write memory card file {0}.\n\n"
|
||||||
|
"Are you running Dolphin from a CD/DVD, or is the save file maybe write protected?\n\n"
|
||||||
|
"Are you receiving this after moving the emulator directory?\nIf so, then you may "
|
||||||
|
"need to re-specify your memory card location in the options.",
|
||||||
|
filename);
|
||||||
|
|
||||||
|
// This flush is unsuccessful.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::SetCurrentThreadName(fmt::format("Memcard {} flushing thread", m_card_slot).c_str());
|
|
||||||
|
|
||||||
const auto flush_interval = std::chrono::seconds(15);
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
// If triggered, we're exiting.
|
std::unique_lock l(m_flush_mutex);
|
||||||
// If timed out, check if we need to flush.
|
memcpy(&m_flush_buffer[0], &m_memcard_data[0], m_memory_card_size);
|
||||||
bool do_exit = m_flush_trigger.WaitFor(flush_interval);
|
|
||||||
if (!do_exit)
|
|
||||||
{
|
|
||||||
bool is_dirty = m_dirty.TestAndClear();
|
|
||||||
if (!is_dirty)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opening the file is purposefully done each iteration to ensure the
|
|
||||||
// file doesn't disappear out from under us after the first check.
|
|
||||||
File::IOFile file(m_filename, "r+b");
|
|
||||||
|
|
||||||
if (!file)
|
|
||||||
{
|
|
||||||
std::string dir;
|
|
||||||
SplitPath(m_filename, &dir, nullptr, nullptr);
|
|
||||||
if (!File::IsDirectory(dir))
|
|
||||||
{
|
|
||||||
File::CreateFullPath(dir);
|
|
||||||
}
|
|
||||||
file.Open(m_filename, "wb");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note - file may have changed above, after ctor
|
|
||||||
if (!file)
|
|
||||||
{
|
|
||||||
PanicAlertFmtT(
|
|
||||||
"Could not write memory card file {0}.\n\n"
|
|
||||||
"Are you running Dolphin from a CD/DVD, or is the save file maybe write protected?\n\n"
|
|
||||||
"Are you receiving this after moving the emulator directory?\nIf so, then you may "
|
|
||||||
"need to re-specify your memory card location in the options.",
|
|
||||||
m_filename);
|
|
||||||
|
|
||||||
// Exit the flushing thread - further flushes will be ignored unless
|
|
||||||
// the thread is recreated.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std::unique_lock l(m_flush_mutex);
|
|
||||||
memcpy(&m_flush_buffer[0], &m_memcard_data[0], m_memory_card_size);
|
|
||||||
}
|
|
||||||
file.WriteBytes(&m_flush_buffer[0], m_memory_card_size);
|
|
||||||
|
|
||||||
if (do_exit)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Core::DisplayMessage(fmt::format("Wrote to Memory Card {}",
|
|
||||||
m_card_slot == ExpansionInterface::Slot::A ? 'A' : 'B'),
|
|
||||||
4000);
|
|
||||||
}
|
}
|
||||||
|
file.WriteBytes(&m_flush_buffer[0], m_memory_card_size);
|
||||||
|
|
||||||
|
Core::DisplayMessage(fmt::format("Wrote to Memory Card {}",
|
||||||
|
m_card_slot == ExpansionInterface::Slot::A ? 'A' : 'B'),
|
||||||
|
4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MemoryCard::MakeDirty()
|
void MemoryCard::MakeDirty()
|
||||||
{
|
{
|
||||||
m_dirty.Set();
|
m_flush_thread.SetDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
s32 MemoryCard::Read(u32 src_address, s32 length, u8* dest_address)
|
s32 MemoryCard::Read(u32 src_address, s32 length, u8* dest_address)
|
||||||
|
@ -203,7 +166,7 @@ void MemoryCard::ClearBlock(u32 address)
|
||||||
PanicAlertFmtT("MemoryCard: ClearBlock called on invalid address ({0:#x})", address);
|
PanicAlertFmtT("MemoryCard: ClearBlock called on invalid address ({0:#x})", address);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
{
|
||||||
std::unique_lock l(m_flush_mutex);
|
std::unique_lock l(m_flush_mutex);
|
||||||
memset(&m_memcard_data[address], 0xFF, Memcard::BLOCK_SIZE);
|
memset(&m_memcard_data[address], 0xFF, Memcard::BLOCK_SIZE);
|
||||||
|
|
|
@ -6,22 +6,19 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
|
||||||
#include "Common/Event.h"
|
#include "Common/FlushThread.h"
|
||||||
#include "Common/Flag.h"
|
|
||||||
#include "Core/HW/GCMemcard/GCMemcard.h"
|
#include "Core/HW/GCMemcard/GCMemcard.h"
|
||||||
#include "Core/HW/GCMemcard/GCMemcardBase.h"
|
#include "Core/HW/GCMemcard/GCMemcardBase.h"
|
||||||
|
|
||||||
class PointerWrap;
|
class PointerWrap;
|
||||||
|
|
||||||
class MemoryCard : public MemoryCardBase
|
class MemoryCard final : public MemoryCardBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MemoryCard(const std::string& filename, ExpansionInterface::Slot card_slot,
|
MemoryCard(std::string filename, ExpansionInterface::Slot card_slot,
|
||||||
u16 size_mbits = Memcard::MBIT_SIZE_MEMORY_CARD_2043);
|
u16 size_mbits = Memcard::MBIT_SIZE_MEMORY_CARD_2043);
|
||||||
~MemoryCard();
|
~MemoryCard() override;
|
||||||
void FlushThread();
|
|
||||||
void MakeDirty();
|
|
||||||
|
|
||||||
s32 Read(u32 src_address, s32 length, u8* dest_address) override;
|
s32 Read(u32 src_address, s32 length, u8* dest_address) override;
|
||||||
s32 Write(u32 dest_address, s32 length, const u8* src_address) override;
|
s32 Write(u32 dest_address, s32 length, const u8* src_address) override;
|
||||||
|
@ -30,18 +27,18 @@ public:
|
||||||
void DoState(PointerWrap& p) override;
|
void DoState(PointerWrap& p) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void FlushToFile(const std::string& filename);
|
||||||
|
void MakeDirty();
|
||||||
|
|
||||||
bool IsAddressInBounds(u32 address, u32 length) const
|
bool IsAddressInBounds(u32 address, u32 length) const
|
||||||
{
|
{
|
||||||
u64 end_address = static_cast<u64>(address) + static_cast<u64>(length);
|
u64 end_address = static_cast<u64>(address) + static_cast<u64>(length);
|
||||||
return end_address <= static_cast<u64>(m_memory_card_size);
|
return end_address <= static_cast<u64>(m_memory_card_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string m_filename;
|
|
||||||
std::unique_ptr<u8[]> m_memcard_data;
|
std::unique_ptr<u8[]> m_memcard_data;
|
||||||
std::unique_ptr<u8[]> m_flush_buffer;
|
std::unique_ptr<u8[]> m_flush_buffer;
|
||||||
std::thread m_flush_thread;
|
Common::FlushThread m_flush_thread;
|
||||||
std::mutex m_flush_mutex;
|
std::mutex m_flush_mutex;
|
||||||
Common::Event m_flush_trigger;
|
|
||||||
Common::Flag m_dirty;
|
|
||||||
u32 m_memory_card_size;
|
u32 m_memory_card_size;
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
<ClInclude Include="Common\FixedSizeQueue.h" />
|
<ClInclude Include="Common\FixedSizeQueue.h" />
|
||||||
<ClInclude Include="Common\Flag.h" />
|
<ClInclude Include="Common\Flag.h" />
|
||||||
<ClInclude Include="Common\FloatUtils.h" />
|
<ClInclude Include="Common\FloatUtils.h" />
|
||||||
|
<ClInclude Include="Common\FlushThread.h" />
|
||||||
<ClInclude Include="Common\FormatUtil.h" />
|
<ClInclude Include="Common\FormatUtil.h" />
|
||||||
<ClInclude Include="Common\FPURoundMode.h" />
|
<ClInclude Include="Common\FPURoundMode.h" />
|
||||||
<ClInclude Include="Common\GekkoDisassembler.h" />
|
<ClInclude Include="Common\GekkoDisassembler.h" />
|
||||||
|
|
|
@ -13,6 +13,7 @@ add_dolphin_test(FileUtilTest FileUtilTest.cpp)
|
||||||
add_dolphin_test(FixedSizeQueueTest FixedSizeQueueTest.cpp)
|
add_dolphin_test(FixedSizeQueueTest FixedSizeQueueTest.cpp)
|
||||||
add_dolphin_test(FlagTest FlagTest.cpp)
|
add_dolphin_test(FlagTest FlagTest.cpp)
|
||||||
add_dolphin_test(FloatUtilsTest FloatUtilsTest.cpp)
|
add_dolphin_test(FloatUtilsTest FloatUtilsTest.cpp)
|
||||||
|
add_dolphin_test(FlushThreadTest FlushThreadTest.cpp)
|
||||||
add_dolphin_test(MathUtilTest MathUtilTest.cpp)
|
add_dolphin_test(MathUtilTest MathUtilTest.cpp)
|
||||||
add_dolphin_test(NandPathsTest NandPathsTest.cpp)
|
add_dolphin_test(NandPathsTest NandPathsTest.cpp)
|
||||||
add_dolphin_test(SettingsHandlerTest SettingsHandlerTest.cpp)
|
add_dolphin_test(SettingsHandlerTest SettingsHandlerTest.cpp)
|
||||||
|
|
79
Source/UnitTests/Common/FlushThreadTest.cpp
Normal file
79
Source/UnitTests/Common/FlushThreadTest.cpp
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// Copyright 2025 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
#include "Common/FlushThread.h"
|
||||||
|
|
||||||
|
TEST(FlushThread, Simple)
|
||||||
|
{
|
||||||
|
Common::FlushThread ft;
|
||||||
|
|
||||||
|
std::atomic<int> value = 0;
|
||||||
|
|
||||||
|
ft.Reset("flush", [&] { ++value; });
|
||||||
|
|
||||||
|
// No flush on start.
|
||||||
|
EXPECT_EQ(value.load(), 0);
|
||||||
|
|
||||||
|
ft.SetDirty();
|
||||||
|
ft.WaitForCompletion();
|
||||||
|
|
||||||
|
// One flush.
|
||||||
|
EXPECT_EQ(value.load(), 1);
|
||||||
|
|
||||||
|
ft.Reset("flush", [&] { ++value; });
|
||||||
|
|
||||||
|
// No change after reset.
|
||||||
|
EXPECT_EQ(value.load(), 1);
|
||||||
|
|
||||||
|
ft.Shutdown();
|
||||||
|
ft.SetDirty();
|
||||||
|
ft.WaitForCompletion();
|
||||||
|
|
||||||
|
// No change because shutdown.
|
||||||
|
EXPECT_EQ(value.load(), 1);
|
||||||
|
|
||||||
|
ft.Reset("flush", [&] {
|
||||||
|
++value;
|
||||||
|
value.notify_one();
|
||||||
|
});
|
||||||
|
ft.WaitForCompletion();
|
||||||
|
|
||||||
|
// Dirty state persits on reset.
|
||||||
|
EXPECT_EQ(value.load(), 2);
|
||||||
|
|
||||||
|
value = 0;
|
||||||
|
|
||||||
|
ft.SetFlushDelay(std::chrono::milliseconds{999999});
|
||||||
|
ft.SetDirty();
|
||||||
|
ft.SetDirty();
|
||||||
|
ft.SetDirty();
|
||||||
|
|
||||||
|
// Not using EXPECT_ here because the tests are technically racey.
|
||||||
|
|
||||||
|
// Probably no flush yet, because of the delay.
|
||||||
|
GTEST_LOG_(INFO) << "Ideally 0: " << value.load();
|
||||||
|
|
||||||
|
const auto start = std::chrono::steady_clock::now();
|
||||||
|
ft.WaitForCompletion();
|
||||||
|
const auto end = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
GTEST_LOG_(INFO) << "Ideally 0: "
|
||||||
|
<< duration_cast<std::chrono::milliseconds>(end - start).count();
|
||||||
|
|
||||||
|
// At least one flush happened. Probably just one.
|
||||||
|
EXPECT_GT(value.load(), 0);
|
||||||
|
GTEST_LOG_(INFO) << "Ideally 1: " << value.load();
|
||||||
|
|
||||||
|
value = 0;
|
||||||
|
|
||||||
|
ft.SetDirty();
|
||||||
|
ft.Reset("flush", [] {});
|
||||||
|
|
||||||
|
// Reset first causes a shutdown, so we have an additional immediate flush.
|
||||||
|
EXPECT_EQ(value.load(), 1);
|
||||||
|
}
|
|
@ -52,6 +52,7 @@
|
||||||
<ClCompile Include="Common\FixedSizeQueueTest.cpp" />
|
<ClCompile Include="Common\FixedSizeQueueTest.cpp" />
|
||||||
<ClCompile Include="Common\FlagTest.cpp" />
|
<ClCompile Include="Common\FlagTest.cpp" />
|
||||||
<ClCompile Include="Common\FloatUtilsTest.cpp" />
|
<ClCompile Include="Common\FloatUtilsTest.cpp" />
|
||||||
|
<ClCompile Include="Common\FlushThreadTest.cpp" />
|
||||||
<ClCompile Include="Common\MathUtilTest.cpp" />
|
<ClCompile Include="Common\MathUtilTest.cpp" />
|
||||||
<ClCompile Include="Common\NandPathsTest.cpp" />
|
<ClCompile Include="Common\NandPathsTest.cpp" />
|
||||||
<ClCompile Include="Common\SettingsHandlerTest.cpp" />
|
<ClCompile Include="Common\SettingsHandlerTest.cpp" />
|
||||||
|
@ -118,4 +119,4 @@
|
||||||
<!--This is only executed via msbuild, VS test runner automatically does this-->
|
<!--This is only executed via msbuild, VS test runner automatically does this-->
|
||||||
<Exec Command="$(TargetPath)" />
|
<Exec Command="$(TargetPath)" />
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue