Files
ppsspp/Core/FileLoaders/RamCachingFileLoader.cpp
T
libretroadmin f0b97a2e90 Remove the standalone frontends
The libretro core is the frontend, and the headless tools are the test and
debugging hosts. The Windows, UWP, Android, iOS, macOS and SDL apps, the UI
framework and imgui go, along with what only they used: the Win32 debugger
hooks (g_frameMutex, the shutdown lock), JNI thread attachment, the Android
text drawer, RAIntegration, Discord, recent files, game installs, tilt input,
the audio resamplers, the control mapper, memstick moving and the remote ISO
and upload server modes. The web server keeps the debugger. The MSVC, Gradle
and ndk-build projects go too.
2026-10-09 04:56:10 +02:00

280 lines
7.6 KiB
C++

// Copyright (c) 2015- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <thread>
#include <cstring>
#include "Common/Thread/ThreadUtil.h"
#include "Common/TimeUtil.h"
#include "Common/Log.h"
#include "Core/FileLoaders/RamCachingFileLoader.h"
// Takes ownership of backend.
RamCachingFileLoader::RamCachingFileLoader(FileLoader *backend)
: ProxiedFileLoader(backend) {
filesize_ = backend->FileSize();
if (filesize_ > 0) {
InitCache();
}
}
RamCachingFileLoader::~RamCachingFileLoader() {
if (filesize_ > 0) {
ShutdownCache();
}
}
bool RamCachingFileLoader::Exists() {
if (exists_ == -1) {
exists_ = ProxiedFileLoader::Exists() ? 1 : 0;
}
return exists_ == 1;
}
bool RamCachingFileLoader::ExistsFast() {
if (exists_ == -1) {
return ProxiedFileLoader::ExistsFast();
}
return exists_ == 1;
}
bool RamCachingFileLoader::IsDirectory() {
if (isDirectory_ == -1) {
isDirectory_ = ProxiedFileLoader::IsDirectory() ? 1 : 0;
}
return isDirectory_ == 1;
}
s64 RamCachingFileLoader::FileSize() {
return filesize_;
}
size_t RamCachingFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags) {
size_t readSize = 0;
if (cache_ == nullptr || (flags & Flags::HINT_UNCACHED) != 0) {
readSize = backend_->ReadAt(absolutePos, bytes, data, flags);
} else {
readSize = ReadFromCache(absolutePos, bytes, data);
// While in case the cache size is too small for the entire read.
while (readSize < bytes) {
SaveIntoCache(absolutePos + readSize, bytes - readSize, flags);
size_t bytesFromCache = ReadFromCache(absolutePos + readSize, bytes - readSize, (u8 *)data + readSize);
readSize += bytesFromCache;
if (bytesFromCache == 0) {
// We can't read any more.
break;
}
}
StartReadAhead(absolutePos + readSize);
}
return readSize;
}
void RamCachingFileLoader::InitCache() {
std::lock_guard<std::mutex> guard(blocksMutex_);
u32 blockCount = (u32)((filesize_ + BLOCK_SIZE - 1) >> BLOCK_SHIFT);
// Overallocate for the last block.
cache_ = (u8 *)malloc((size_t)blockCount << BLOCK_SHIFT);
if (cache_ == nullptr) {
ERROR_LOG(Log::IO, "Failed to allocate cache for Cache full ISO in RAM! Will fall back to regular reads.");
return;
}
aheadRemaining_ = blockCount;
blocks_.resize(blockCount);
}
void RamCachingFileLoader::ShutdownCache() {
Cancel();
// We can't delete while the thread is running, so have to wait.
// This should only happen from the menu.
if (aheadThread_.joinable())
aheadThread_.join();
_dbg_assert_(!aheadThreadRunning_);
std::lock_guard<std::mutex> guard(blocksMutex_);
blocks_.clear();
if (cache_ != nullptr) {
free(cache_);
cache_ = nullptr;
}
}
void RamCachingFileLoader::Cancel() {
if (aheadThreadRunning_) {
std::lock_guard<std::mutex> guard(blocksMutex_);
aheadCancel_ = true;
}
ProxiedFileLoader::Cancel();
}
size_t RamCachingFileLoader::ReadFromCache(s64 pos, size_t bytes, void *data) {
s64 cacheStartPos = pos >> BLOCK_SHIFT;
s64 cacheEndPos = (pos + bytes - 1) >> BLOCK_SHIFT;
if ((size_t)cacheEndPos >= blocks_.size()) {
cacheEndPos = blocks_.size() - 1;
}
size_t readSize = 0;
size_t offset = (size_t)(pos - (cacheStartPos << BLOCK_SHIFT));
u8 *p = (u8 *)data;
// Clamp bytes to what's actually available.
if (pos + (s64)bytes > filesize_) {
// Should've been caught above, but just in case.
if (pos >= filesize_) {
return 0;
}
bytes = (size_t)(filesize_ - pos);
}
std::lock_guard<std::mutex> guard(blocksMutex_);
for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) {
if (blocks_[(size_t)i] == 0) {
return readSize;
}
size_t toRead = std::min(bytes - readSize, (size_t)BLOCK_SIZE - offset);
s64 cachePos = (i << BLOCK_SHIFT) + offset;
memcpy(p + readSize, &cache_[cachePos], toRead);
readSize += toRead;
// Don't need an offset after the first read.
offset = 0;
}
return readSize;
}
void RamCachingFileLoader::SaveIntoCache(s64 pos, size_t bytes, Flags flags) {
s64 cacheStartPos = pos >> BLOCK_SHIFT;
s64 cacheEndPos = (pos + bytes - 1) >> BLOCK_SHIFT;
if ((size_t)cacheEndPos >= blocks_.size()) {
cacheEndPos = blocks_.size() - 1;
}
size_t blocksToRead = 0;
{
std::lock_guard<std::mutex> guard(blocksMutex_);
for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) {
if (blocks_[(size_t)i] == 0) {
++blocksToRead;
if (blocksToRead >= MAX_BLOCKS_PER_READ) {
break;
}
// TODO: Shouldn't we break as soon as we see a 1?
}
}
}
s64 cacheFilePos = cacheStartPos << BLOCK_SHIFT;
size_t bytesRead = backend_->ReadAt(cacheFilePos, blocksToRead << BLOCK_SHIFT, &cache_[cacheFilePos], flags);
// In case there was an error, let's not mark blocks that failed to read as read.
// Round up only for a genuine short read exactly at the true end of the file -
// cache_ is deliberately over-allocated to a full BLOCK_SIZE for the last block,
// so its unwritten tail past filesize_ is never read back. Any other short read
// (e.g. a dropped Remote ISO connection mid-file) must not be rounded up, or the
// unwritten (uninitialized, since cache_ is malloc'd) rest of that block would be
// served as if it were real file data.
u32 blocksActuallyRead = (u32)(bytesRead >> BLOCK_SHIFT);
if ((bytesRead & (BLOCK_SIZE - 1)) != 0 && cacheFilePos + (s64)bytesRead == filesize_) {
++blocksActuallyRead;
}
{
std::lock_guard<std::mutex> guard(blocksMutex_);
// In case they were simultaneously read.
u32 blocksRead = 0;
for (size_t i = 0; i < blocksActuallyRead; ++i) {
if (blocks_[(size_t)cacheStartPos + i] == 0) {
blocks_[(size_t)cacheStartPos + i] = 1;
++blocksRead;
}
}
if (aheadRemaining_ != 0) {
aheadRemaining_ -= blocksRead;
}
}
}
void RamCachingFileLoader::StartReadAhead(s64 pos) {
if (cache_ == nullptr) {
return;
}
std::lock_guard<std::mutex> guard(blocksMutex_);
aheadPos_ = pos;
if (aheadThreadRunning_) {
// Already going.
return;
}
aheadThreadRunning_ = true;
aheadCancel_ = false;
if (aheadThread_.joinable())
aheadThread_.join();
aheadThread_ = std::thread([this] {
SetCurrentThreadName("FileLoaderReadAhead");
while (aheadRemaining_ != 0 && !aheadCancel_) {
// Where should we look?
const u32 cacheStartPos = NextAheadBlock();
if (cacheStartPos == 0xFFFFFFFF) {
// Must be full.
break;
}
u32 cacheEndPos = cacheStartPos + BLOCK_READAHEAD - 1;
if (cacheEndPos >= blocks_.size()) {
cacheEndPos = (u32)blocks_.size() - 1;
}
for (u32 i = cacheStartPos; i <= cacheEndPos; ++i) {
if (blocks_[i] == 0) {
SaveIntoCache((u64)i << BLOCK_SHIFT, BLOCK_SIZE * BLOCK_READAHEAD, Flags::NONE);
break;
}
}
}
aheadThreadRunning_ = false;
});
}
u32 RamCachingFileLoader::NextAheadBlock() {
std::lock_guard<std::mutex> guard(blocksMutex_);
// If we had an aheadPos_ set, start reading from there and go forward.
u32 startFrom = (u32)(aheadPos_ >> BLOCK_SHIFT);
// But next time, start from the beginning again.
aheadPos_ = 0;
for (u32 i = startFrom; i < blocks_.size(); ++i) {
if (blocks_[i] == 0) {
return i;
}
}
return 0xFFFFFFFF;
}