mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Android: flush memory card writes when the app is backgrounded
A memory card write does not necessarily reach the file system when it happens,
and on Android that is data loss rather than a detail.
A FOLDER card holds writes in an in-memory page cache and flushes two frames
after the last one, counted down by the per-frame tick that runs off vsync. So
pausing does not delay that flush, it stops it ever being reached -- the counter
does not advance at all while the VM is paused.
A FILE card writes through stdio with no flush anywhere in the path. Seeking on
an update stream pushes the previous write out, so a run of writes mostly
self-corrects, but the last write of a save sequence sits in the buffer until the
next card access or fclose.
Either way the pending write is lost if Android reclaims the process while it is
backgrounded, which it may do with no further callback. Save in-game, switch
apps, get reclaimed, and the save was never on disk.
The pause path already handles exactly this shape for the BIOS NVRAM
(cdvdSaveNVRAM, added because "the process is frequently killed while paused"),
so the card flush goes next to it, in both the Running and already-Paused
branches. It runs on the CPU thread, queued after SetPaused, so the console is
stopped and nothing can be written behind it, and it is fire-and-forget -- onPause
is on a deadline and blocking it risks an ANR.
- FileMcd_Flush() / FileMemoryCard::Flush() / FolderMemoryCardAggregator::Flush()
write out what is buffered without closing anything, so the console keeps
playing afterwards.
- FileMemoryCard::Flush deliberately does NOT stamp the running checksum the way
Close() does. That value is a change-detector a savestate load compares to
decide whether the card moved under the console, not an integrity check, and
m_chkaddr is card data rather than a header field we own. A stale value costs
one auto-eject on the next savestate load, which is the safe direction, so
writing to the card on a path upstream never writes on buys nothing.
- FolderMemoryCard::FlushNow clears the frame countdown so a resumed VM does not
repeat the work. Flush() is already a no-op when nothing is cached, so calling
this on a quiet card costs nothing.
- Save() flushes each sector as it is written.
Also bounds the emulation-thread join in onDestroy. NativeApp.shutdown() already
gives up waiting after 5 s and returns anyway, so an unbounded join inherited a
wedged CPU thread and hung the destroy path until Android force-closed us.
This commit is contained in:
@@ -175,6 +175,7 @@ public:
|
||||
|
||||
void Open();
|
||||
void Close();
|
||||
void Flush();
|
||||
|
||||
s32 IsPresent(uint slot);
|
||||
void GetSizeInfo(uint slot, McdSizeInfo& outways);
|
||||
@@ -343,6 +344,27 @@ void FileMemoryCard::Open()
|
||||
}
|
||||
}
|
||||
|
||||
// Write everything still buffered for every open card out to the host file system, without
|
||||
// closing anything. The console keeps playing afterwards; this exists so a host that is about to
|
||||
// lose the process -- Android backgrounding the app, which can then be reclaimed with no further
|
||||
// callback -- can bank what has been written so far.
|
||||
void FileMemoryCard::Flush()
|
||||
{
|
||||
for (int slot = 0; slot < 8; ++slot)
|
||||
{
|
||||
if (!m_file[slot])
|
||||
continue;
|
||||
|
||||
// Deliberately NOT the checksum store Close() does. That value is not an integrity
|
||||
// check on the card; it is a change-detector a savestate load compares to decide
|
||||
// whether the card moved under the console. Stamping it here would write to
|
||||
// m_chkaddr, which is card data rather than a header field we own, on a path
|
||||
// upstream never writes on -- and it would buy nothing, because a stale value only
|
||||
// costs one auto-eject on the next savestate load, which is the safe direction.
|
||||
std::fflush(m_file[slot]);
|
||||
}
|
||||
}
|
||||
|
||||
void FileMemoryCard::Close()
|
||||
{
|
||||
for (int slot = 0; slot < 8; ++slot)
|
||||
@@ -497,6 +519,14 @@ s32 FileMemoryCard::Save(uint slot, const u8* src, u32 adr, int size)
|
||||
|
||||
if (std::fwrite(m_currentdata.data(), size, 1, mcfp) == 1)
|
||||
{
|
||||
// Get the sector out of the stdio buffer now. Nothing else in this path flushes, so
|
||||
// the last write of a save sequence would otherwise sit in the buffer until the next
|
||||
// card access or fclose. On a host that can kill the process while the VM is paused
|
||||
// -- Android does exactly that -- losing that one sector is a half-written save. This
|
||||
// hands the bytes to the OS; it is not a disk sync, and costs nothing measurable at
|
||||
// memory card write rates.
|
||||
std::fflush(mcfp);
|
||||
|
||||
static auto last = std::chrono::time_point<std::chrono::system_clock>();
|
||||
|
||||
std::chrono::duration<float> elapsed = std::chrono::system_clock::now() - last;
|
||||
@@ -631,6 +661,14 @@ void FileMcd_EmuClose()
|
||||
Mcd::impl.Close();
|
||||
}
|
||||
|
||||
void FileMcd_Flush()
|
||||
{
|
||||
if (!FileMcd_Open)
|
||||
return;
|
||||
Mcd::implFolder.Flush();
|
||||
Mcd::impl.Flush();
|
||||
}
|
||||
|
||||
void FileMcd_CancelEject()
|
||||
{
|
||||
AutoEject::ClearAll();
|
||||
|
||||
@@ -40,6 +40,9 @@ uint FileMcd_ConvertToSlot(uint port, uint slot);
|
||||
void FileMcd_SetType();
|
||||
void FileMcd_EmuOpen();
|
||||
void FileMcd_EmuClose();
|
||||
// Write out everything buffered for the open cards without closing them, so a host that is about
|
||||
// to lose the process does not lose the writes that have not reached the file system yet.
|
||||
void FileMcd_Flush();
|
||||
void FileMcd_CancelEject();
|
||||
void FileMcd_Reopen(std::string new_serial);
|
||||
void FileMcd_Swap();
|
||||
|
||||
@@ -1075,6 +1075,21 @@ void FolderMemoryCard::NextFrame()
|
||||
}
|
||||
}
|
||||
|
||||
void FolderMemoryCard::FlushNow()
|
||||
{
|
||||
if (!m_isEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The countdown above is driven by NextFrame(), which runs off vsync -- so while the VM is
|
||||
// paused it does not advance at all and the pending flush is never reached, rather than
|
||||
// merely being late. Clear it so a resumed VM does not immediately repeat this work.
|
||||
// Flush() itself is a no-op when nothing is cached, so calling this on a quiet card is free.
|
||||
m_framesUntilFlush = 0;
|
||||
Flush();
|
||||
}
|
||||
|
||||
void FolderMemoryCard::Flush()
|
||||
{
|
||||
if (m_cache.empty())
|
||||
@@ -2313,6 +2328,14 @@ void FolderMemoryCardAggregator::Close()
|
||||
}
|
||||
}
|
||||
|
||||
void FolderMemoryCardAggregator::Flush()
|
||||
{
|
||||
for (int i = 0; i < TotalCardSlots; ++i)
|
||||
{
|
||||
m_cards[i].FlushNow();
|
||||
}
|
||||
}
|
||||
|
||||
void FolderMemoryCardAggregator::SetFiltering(const bool enableFiltering)
|
||||
{
|
||||
m_enableFiltering = enableFiltering;
|
||||
|
||||
@@ -318,6 +318,10 @@ public:
|
||||
void Open(std::string fullPath, const Pcsx2Config::McdOptions& mcdOptions, const u32 sizeInClusters, const bool enableFiltering, std::string filter, bool simulateFileWrites = false);
|
||||
// Close the memory card and flush changes to the file system. Set flush to false to not store changes.
|
||||
void Close(bool flush = true);
|
||||
// Write any cached writes out to the file system now, rather than waiting out the
|
||||
// post-write frame countdown -- which only advances while frames are being drawn, so a
|
||||
// paused VM never reaches it.
|
||||
void FlushNow();
|
||||
// Checks whether the Memory Card is formatted.
|
||||
bool IsFormatted() const;
|
||||
|
||||
@@ -546,6 +550,8 @@ public:
|
||||
|
||||
void Open();
|
||||
void Close();
|
||||
// Flush every slot's cached writes without closing anything.
|
||||
void Flush();
|
||||
|
||||
void SetFiltering(const bool enableFiltering);
|
||||
|
||||
|
||||
@@ -2797,6 +2797,19 @@ Java_kr_co_iefriends_pcsx2_NativeApp_pause(JNIEnv *env, jclass clazz) {
|
||||
// no-ops when the NVM is unchanged, so pausing repeatedly is cheap.
|
||||
if (VMManager::HasValidVM())
|
||||
cdvdSaveNVRAM();
|
||||
// Same reasoning as the NVRAM above, and the same failure: a memory card write does
|
||||
// not necessarily reach the file system when it happens. A FOLDER card holds writes
|
||||
// in an in-memory page cache and flushes two frames after the last one, counted down
|
||||
// by the per-frame tick that runs off vsync — so pausing does not delay that flush,
|
||||
// it stops it being reached at all. A FILE card writes through, but the last sector
|
||||
// of a save sequence sits in the stdio buffer until the next card access.
|
||||
//
|
||||
// Either way the pending write is lost if Android reclaims the process while it is
|
||||
// backgrounded, which it is free to do with no further callback. Save in-game, switch
|
||||
// apps, get reclaimed — and the save was never on disk. Queued after SetPaused above,
|
||||
// so the console is stopped and nothing can be written behind us.
|
||||
if (VMManager::HasValidVM())
|
||||
FileMcd_Flush();
|
||||
});
|
||||
|
||||
if (!s_execute_exit.load(std::memory_order_acquire) && Cpu)
|
||||
@@ -2816,8 +2829,10 @@ Java_kr_co_iefriends_pcsx2_NativeApp_pause(JNIEnv *env, jclass clazz) {
|
||||
Host::RunOnCPUThread([]() {
|
||||
if (VMManager::HasValidVM())
|
||||
cdvdSaveNVRAM();
|
||||
if (VMManager::HasValidVM())
|
||||
FileMcd_Flush();
|
||||
});
|
||||
Console.WriteLn("@@ANDROID_PAUSE@@ already_paused nvm_flush_queued");
|
||||
Console.WriteLn("@@ANDROID_PAUSE@@ already_paused nvm_and_mcd_flush_queued");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
/** Upper bound on waiting for the emulation thread after shutdown has been asked for.
|
||||
* NativeApp.shutdown() already waits 5 s for the VM to reach Shutdown and returns anyway if
|
||||
* it does not, so an unbounded join here inherits a wedged CPU thread and hangs the destroy
|
||||
* path until Android force-closes us. In the ordinary case the thread is already gone and
|
||||
* this costs nothing. */
|
||||
private static final long EMU_THREAD_JOIN_TIMEOUT_MS = 2000L;
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
NativeApp.shutdown();
|
||||
@@ -218,7 +225,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
////
|
||||
if (mEmulationThread != null) {
|
||||
try {
|
||||
mEmulationThread.join();
|
||||
mEmulationThread.join(EMU_THREAD_JOIN_TIMEOUT_MS);
|
||||
if (mEmulationThread.isAlive()) {
|
||||
// Nothing more we can do from here — the VM did not unwind, so the memory
|
||||
// cards were not closed by FileMcd_EmuClose either. onPause has already
|
||||
// queued a card flush by this point in the normal lifecycle, which is what
|
||||
// keeps this from being data loss.
|
||||
NativeApp.emulog("onDestroy: emulation thread still alive after "
|
||||
+ EMU_THREAD_JOIN_TIMEOUT_MS + "ms, killing anyway");
|
||||
}
|
||||
mEmulationThread = null;
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user