mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 17:55:23 +02:00
blocksActuallyRead rounded bytesRead up to the next whole block unconditionally, intending to handle the legitimate case where the very last block of the file is naturally shorter than BLOCK_SIZE (cache_ is deliberately over-allocated for that). But it applied the same rounding to any short read, including a genuine failure or a dropped connection mid-file (this loader can sit on top of the whole Remote ISO chain via CachingFileLoader/HTTPFileLoader when "Cache full ISO in RAM" is enabled) - marking a block as fully cached when only a few of its bytes were actually written. Since cache_ is malloc'd (not zeroed), every later read of that block would serve uninitialized heap memory as if it were real file data. Only round up when the short read's end position exactly matches the true end of the file.
281 lines
7.7 KiB
C++
281 lines
7.7 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");
|
|
|
|
AndroidJNIThreadContext jniContext;
|
|
|
|
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;
|
|
}
|