mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-02-02 11:02:28 +02:00
Merge pull request #6565 from leoetlino/fs-timing
IOS: Emulate filesystem timings
This commit is contained in:
commit
75574b7b97
@ -4,6 +4,7 @@
|
||||
|
||||
#include "Core/IOS/FS/FileSystemProxy.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
@ -29,12 +30,20 @@ static s32 ConvertResult(ResultCode code)
|
||||
return -(static_cast<s32>(code) + 100);
|
||||
}
|
||||
|
||||
// XXX: timing is not the same for all commands and in all cases.
|
||||
static IPCCommandResult GetFSReply(s32 return_value)
|
||||
static IPCCommandResult GetFSReply(s32 return_value, u64 extra_tb_ticks = 0)
|
||||
{
|
||||
return {return_value, true, SystemTimers::GetTicksPerSecond() / 500};
|
||||
// According to hardware tests, FS takes at least 2700 TB ticks to reply to commands.
|
||||
return {return_value, true, (2700 + extra_tb_ticks) * SystemTimers::TIMER_RATIO};
|
||||
}
|
||||
|
||||
/// Amount of TB ticks required for a superblock write to complete.
|
||||
constexpr u64 SUPERBLOCK_WRITE_TICKS = 3370000;
|
||||
/// Amount of TB ticks required to write a cluster (for a file).
|
||||
constexpr u64 CLUSTER_WRITE_TICKS = 300000;
|
||||
/// Amount of TB ticks required to read a cluster (for a file).
|
||||
constexpr u64 CLUSTER_READ_TICKS = 115000;
|
||||
constexpr size_t CLUSTER_DATA_SIZE = 0x4000;
|
||||
|
||||
FS::FS(Kernel& ios, const std::string& device_name) : Device(ios, device_name)
|
||||
{
|
||||
}
|
||||
@ -42,6 +51,9 @@ FS::FS(Kernel& ios, const std::string& device_name) : Device(ios, device_name)
|
||||
void FS::DoState(PointerWrap& p)
|
||||
{
|
||||
p.Do(m_fd_map);
|
||||
p.Do(m_cache_fd);
|
||||
p.Do(m_cache_chain_index);
|
||||
p.Do(m_dirty_cache);
|
||||
}
|
||||
|
||||
static void LogResult(const std::string& command, ResultCode code)
|
||||
@ -56,35 +68,80 @@ static void LogResult(const std::string& command, const Result<T>& result)
|
||||
LogResult(command, result.Succeeded() ? ResultCode::Success : result.Error());
|
||||
}
|
||||
|
||||
enum class FileLookupMode
|
||||
{
|
||||
Normal,
|
||||
/// Timing model to use when FS splits the path into a parent and a file name
|
||||
/// and looks up each of them individually.
|
||||
Split,
|
||||
};
|
||||
// Note: lookups normally stop at the first non existing path (as the FST cannot be traversed
|
||||
// further when a directory doesn't exist). However for the sake of simplicity we assume that the
|
||||
// entire lookup succeeds because otherwise we'd need to check whether every path component exists.
|
||||
static u64 EstimateFileLookupTicks(const std::string& path, FileLookupMode mode)
|
||||
{
|
||||
const size_t number_of_path_components = std::count(path.cbegin(), path.cend(), '/');
|
||||
if (number_of_path_components == 0)
|
||||
return 0;
|
||||
// Paths that end with a slash are invalid and rejected early in FS.
|
||||
if (!path.empty() && *path.rbegin() == '/')
|
||||
return 300;
|
||||
if (mode == FileLookupMode::Normal)
|
||||
return 680 * number_of_path_components;
|
||||
return 1000 + 340 * number_of_path_components;
|
||||
}
|
||||
|
||||
/// Get a reply with the correct timing for operations that modify the superblock.
|
||||
///
|
||||
/// A superblock flush takes a very large amount of time, so other delays are ignored
|
||||
/// to simplify the implementation as they are insignificant.
|
||||
static IPCCommandResult GetReplyForSuperblockOperation(ResultCode result)
|
||||
{
|
||||
const u64 ticks = result == ResultCode::Success ? SUPERBLOCK_WRITE_TICKS : 0;
|
||||
return GetFSReply(ConvertResult(result), ticks);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::Open(const OpenRequest& request)
|
||||
{
|
||||
if (m_fd_map.size() >= 16)
|
||||
return GetDefaultReply(ConvertResult(ResultCode::NoFreeHandle));
|
||||
return GetFSReply(ConvertResult(ResultCode::NoFreeHandle));
|
||||
|
||||
if (request.path.size() >= 64)
|
||||
return GetDefaultReply(ConvertResult(ResultCode::Invalid));
|
||||
return GetFSReply(ConvertResult(ResultCode::Invalid));
|
||||
|
||||
if (request.path == "/dev/fs")
|
||||
{
|
||||
m_fd_map[request.fd] = {request.gid, request.uid, INVALID_FD};
|
||||
return GetDefaultReply(IPC_SUCCESS);
|
||||
return GetFSReply(IPC_SUCCESS);
|
||||
}
|
||||
|
||||
const u64 ticks = EstimateFileLookupTicks(request.path, FileLookupMode::Normal);
|
||||
|
||||
auto backend_fd = m_ios.GetFS()->OpenFile(request.uid, request.gid, request.path,
|
||||
static_cast<Mode>(request.flags & 3));
|
||||
LogResult(StringFromFormat("OpenFile(%s)", request.path.c_str()), backend_fd);
|
||||
if (!backend_fd)
|
||||
return GetFSReply(ConvertResult(backend_fd.Error()));
|
||||
return GetFSReply(ConvertResult(backend_fd.Error()), ticks);
|
||||
|
||||
m_fd_map[request.fd] = {request.gid, request.uid, backend_fd->Release()};
|
||||
std::strncpy(m_fd_map[request.fd].name.data(), request.path.c_str(), 64);
|
||||
return GetFSReply(IPC_SUCCESS);
|
||||
return GetFSReply(IPC_SUCCESS, ticks);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::Close(u32 fd)
|
||||
{
|
||||
u64 ticks = 0;
|
||||
if (m_fd_map[fd].fs_fd != INVALID_FD)
|
||||
{
|
||||
if (fd == m_cache_fd)
|
||||
{
|
||||
ticks += SimulateFlushFileCache();
|
||||
m_cache_fd = INVALID_FD;
|
||||
}
|
||||
|
||||
if (m_fd_map[fd].superblock_flush_needed)
|
||||
ticks += SUPERBLOCK_WRITE_TICKS;
|
||||
|
||||
const ResultCode result = m_ios.GetFS()->Close(m_fd_map[fd].fs_fd);
|
||||
LogResult(StringFromFormat("Close(%s)", m_fd_map[fd].name.data()), result);
|
||||
m_fd_map.erase(fd);
|
||||
@ -95,14 +152,89 @@ IPCCommandResult FS::Close(u32 fd)
|
||||
{
|
||||
m_fd_map.erase(fd);
|
||||
}
|
||||
return GetFSReply(IPC_SUCCESS);
|
||||
return GetFSReply(IPC_SUCCESS, ticks);
|
||||
}
|
||||
|
||||
u64 FS::SimulatePopulateFileCache(u32 fd, u32 offset, u32 file_size)
|
||||
{
|
||||
if (HasCacheForFile(fd, offset))
|
||||
return 0;
|
||||
|
||||
u64 ticks = SimulateFlushFileCache();
|
||||
if ((offset % CLUSTER_DATA_SIZE != 0 || offset != file_size) && offset < file_size)
|
||||
ticks += CLUSTER_READ_TICKS;
|
||||
|
||||
m_cache_fd = fd;
|
||||
m_cache_chain_index = static_cast<u16>(offset / CLUSTER_DATA_SIZE);
|
||||
return ticks;
|
||||
}
|
||||
|
||||
u64 FS::SimulateFlushFileCache()
|
||||
{
|
||||
if (m_cache_fd == INVALID_FD || !m_dirty_cache)
|
||||
return 0;
|
||||
m_dirty_cache = false;
|
||||
m_fd_map[m_cache_fd].superblock_flush_needed = true;
|
||||
return CLUSTER_WRITE_TICKS;
|
||||
}
|
||||
|
||||
// Simulate parts of the FS read/write logic to estimate ticks for file operations correctly.
|
||||
u64 FS::EstimateTicksForReadWrite(const Handle& handle, const ReadWriteRequest& request)
|
||||
{
|
||||
u64 ticks = 0;
|
||||
|
||||
const bool is_write = request.command == IPC_CMD_WRITE;
|
||||
const Result<FileStatus> status = m_ios.GetFS()->GetFileStatus(handle.fs_fd);
|
||||
u32 offset = status->offset;
|
||||
u32 count = request.size;
|
||||
if (!is_write && count + offset > status->size)
|
||||
count = status->size - offset;
|
||||
|
||||
while (count != 0)
|
||||
{
|
||||
u32 copy_length;
|
||||
// Fast path (if not cached): FS copies an entire cluster directly from/to the request.
|
||||
if (!HasCacheForFile(request.fd, offset) && count >= CLUSTER_DATA_SIZE &&
|
||||
offset % CLUSTER_DATA_SIZE == 0)
|
||||
{
|
||||
ticks += is_write ? CLUSTER_WRITE_TICKS : CLUSTER_READ_TICKS;
|
||||
copy_length = CLUSTER_DATA_SIZE;
|
||||
if (is_write)
|
||||
m_fd_map[request.fd].superblock_flush_needed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ticks += SimulatePopulateFileCache(request.fd, offset, status->size);
|
||||
|
||||
const u32 start = offset - m_cache_chain_index * CLUSTER_DATA_SIZE;
|
||||
copy_length = std::min<u32>(CLUSTER_DATA_SIZE - start, count);
|
||||
// FS essentially just does a memcpy from/to an internal buffer.
|
||||
ticks += 0.63f * copy_length + (request.command == IPC_CMD_WRITE ? 1000 : 0);
|
||||
m_dirty_cache = is_write;
|
||||
|
||||
if (is_write && (offset + copy_length) % CLUSTER_DATA_SIZE == 0)
|
||||
ticks += SimulateFlushFileCache();
|
||||
}
|
||||
offset += copy_length;
|
||||
count -= copy_length;
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
bool FS::HasCacheForFile(u32 fd, u32 offset) const
|
||||
{
|
||||
const u16 chain_index = static_cast<u16>(offset / CLUSTER_DATA_SIZE);
|
||||
return m_cache_fd == fd && m_cache_chain_index == chain_index;
|
||||
}
|
||||
|
||||
IPCCommandResult FS::Read(const ReadWriteRequest& request)
|
||||
{
|
||||
const Handle& handle = m_fd_map[request.fd];
|
||||
if (handle.fs_fd == INVALID_FD)
|
||||
return GetDefaultReply(ConvertResult(ResultCode::Invalid));
|
||||
return GetFSReply(ConvertResult(ResultCode::Invalid));
|
||||
|
||||
// Simulate the FS read logic to estimate ticks. Note: this must be done before reading.
|
||||
const u64 ticks = EstimateTicksForReadWrite(handle, request);
|
||||
|
||||
const Result<u32> result = m_ios.GetFS()->ReadBytesFromFile(
|
||||
handle.fs_fd, Memory::GetPointer(request.buffer), request.size);
|
||||
@ -110,15 +242,19 @@ IPCCommandResult FS::Read(const ReadWriteRequest& request)
|
||||
StringFromFormat("Read(%s, 0x%08x, %u)", handle.name.data(), request.buffer, request.size),
|
||||
result);
|
||||
if (!result)
|
||||
return GetDefaultReply(ConvertResult(result.Error()));
|
||||
return GetDefaultReply(*result);
|
||||
return GetFSReply(ConvertResult(result.Error()));
|
||||
|
||||
return GetFSReply(*result, ticks);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::Write(const ReadWriteRequest& request)
|
||||
{
|
||||
const Handle& handle = m_fd_map[request.fd];
|
||||
if (handle.fs_fd == INVALID_FD)
|
||||
return GetDefaultReply(ConvertResult(ResultCode::Invalid));
|
||||
return GetFSReply(ConvertResult(ResultCode::Invalid));
|
||||
|
||||
// Simulate the FS write logic to estimate ticks. Must be done before writing.
|
||||
const u64 ticks = EstimateTicksForReadWrite(handle, request);
|
||||
|
||||
const Result<u32> result = m_ios.GetFS()->WriteBytesToFile(
|
||||
handle.fs_fd, Memory::GetPointer(request.buffer), request.size);
|
||||
@ -126,15 +262,16 @@ IPCCommandResult FS::Write(const ReadWriteRequest& request)
|
||||
StringFromFormat("Write(%s, 0x%08x, %u)", handle.name.data(), request.buffer, request.size),
|
||||
result);
|
||||
if (!result)
|
||||
return GetDefaultReply(ConvertResult(result.Error()));
|
||||
return GetDefaultReply(*result);
|
||||
return GetFSReply(ConvertResult(result.Error()));
|
||||
|
||||
return GetFSReply(*result, ticks);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::Seek(const SeekRequest& request)
|
||||
{
|
||||
const Handle& handle = m_fd_map[request.fd];
|
||||
if (handle.fs_fd == INVALID_FD)
|
||||
return GetDefaultReply(ConvertResult(ResultCode::Invalid));
|
||||
return GetFSReply(ConvertResult(ResultCode::Invalid));
|
||||
|
||||
const Result<u32> result =
|
||||
m_ios.GetFS()->SeekFile(handle.fs_fd, request.offset, IOS::HLE::FS::SeekMode(request.mode));
|
||||
@ -142,8 +279,8 @@ IPCCommandResult FS::Seek(const SeekRequest& request)
|
||||
StringFromFormat("Seek(%s, 0x%08x, %u)", handle.name.data(), request.offset, request.mode),
|
||||
result);
|
||||
if (!result)
|
||||
return GetDefaultReply(ConvertResult(result.Error()));
|
||||
return GetDefaultReply(*result);
|
||||
return GetFSReply(ConvertResult(result.Error()));
|
||||
return GetFSReply(*result);
|
||||
}
|
||||
|
||||
#pragma pack(push, 1)
|
||||
@ -245,7 +382,7 @@ IPCCommandResult FS::Format(const Handle& handle, const IOCtlRequest& request)
|
||||
return GetFSReply(ConvertResult(ResultCode::AccessDenied));
|
||||
|
||||
const ResultCode result = m_ios.GetFS()->Format(handle.uid);
|
||||
return GetFSReply(ConvertResult(result));
|
||||
return GetReplyForSuperblockOperation(result);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::GetStats(const Handle& handle, const IOCtlRequest& request)
|
||||
@ -280,7 +417,7 @@ IPCCommandResult FS::CreateDirectory(const Handle& handle, const IOCtlRequest& r
|
||||
m_ios.GetFS()->CreateDirectory(handle.uid, handle.gid, params->path, params->attribute,
|
||||
params->owner_mode, params->group_mode, params->other_mode);
|
||||
LogResult(StringFromFormat("CreateDirectory(%s)", params->path), result);
|
||||
return GetFSReply(ConvertResult(result));
|
||||
return GetReplyForSuperblockOperation(result);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::ReadDirectory(const Handle& handle, const IOCtlVRequest& request)
|
||||
@ -345,7 +482,7 @@ IPCCommandResult FS::SetAttribute(const Handle& handle, const IOCtlRequest& requ
|
||||
handle.uid, params->path, params->uid, params->gid, params->attribute, params->owner_mode,
|
||||
params->group_mode, params->other_mode);
|
||||
LogResult(StringFromFormat("SetMetadata(%s)", params->path), result);
|
||||
return GetFSReply(ConvertResult(result));
|
||||
return GetReplyForSuperblockOperation(result);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::GetAttribute(const Handle& handle, const IOCtlRequest& request)
|
||||
@ -354,10 +491,11 @@ IPCCommandResult FS::GetAttribute(const Handle& handle, const IOCtlRequest& requ
|
||||
return GetFSReply(ConvertResult(ResultCode::Invalid));
|
||||
|
||||
const std::string path = Memory::GetString(request.buffer_in, 64);
|
||||
const u64 ticks = EstimateFileLookupTicks(path, FileLookupMode::Split);
|
||||
const Result<Metadata> metadata = m_ios.GetFS()->GetMetadata(handle.uid, handle.gid, path);
|
||||
LogResult(StringFromFormat("GetMetadata(%s)", path.c_str()), metadata);
|
||||
if (!metadata)
|
||||
return GetFSReply(ConvertResult(metadata.Error()));
|
||||
return GetFSReply(ConvertResult(metadata.Error()), ticks);
|
||||
|
||||
// Yes, the other members aren't copied at all. Actually, IOS does not even memset
|
||||
// the struct at all, which means uninitialised bytes from the stack are returned.
|
||||
@ -370,7 +508,7 @@ IPCCommandResult FS::GetAttribute(const Handle& handle, const IOCtlRequest& requ
|
||||
out.group_mode = metadata->group_mode;
|
||||
out.other_mode = metadata->other_mode;
|
||||
Memory::CopyToEmu(request.buffer_out, &out, sizeof(out));
|
||||
return GetFSReply(IPC_SUCCESS);
|
||||
return GetFSReply(IPC_SUCCESS, ticks);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::DeleteFile(const Handle& handle, const IOCtlRequest& request)
|
||||
@ -381,7 +519,7 @@ IPCCommandResult FS::DeleteFile(const Handle& handle, const IOCtlRequest& reques
|
||||
const std::string path = Memory::GetString(request.buffer_in, 64);
|
||||
const ResultCode result = m_ios.GetFS()->Delete(handle.uid, handle.gid, path);
|
||||
LogResult(StringFromFormat("Delete(%s)", path.c_str()), result);
|
||||
return GetFSReply(ConvertResult(result));
|
||||
return GetReplyForSuperblockOperation(result);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::RenameFile(const Handle& handle, const IOCtlRequest& request)
|
||||
@ -393,7 +531,7 @@ IPCCommandResult FS::RenameFile(const Handle& handle, const IOCtlRequest& reques
|
||||
const std::string new_path = Memory::GetString(request.buffer_in + 64, 64);
|
||||
const ResultCode result = m_ios.GetFS()->Rename(handle.uid, handle.gid, old_path, new_path);
|
||||
LogResult(StringFromFormat("Rename(%s, %s)", old_path.c_str(), new_path.c_str()), result);
|
||||
return GetFSReply(ConvertResult(result));
|
||||
return GetReplyForSuperblockOperation(result);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::CreateFile(const Handle& handle, const IOCtlRequest& request)
|
||||
@ -406,7 +544,7 @@ IPCCommandResult FS::CreateFile(const Handle& handle, const IOCtlRequest& reques
|
||||
m_ios.GetFS()->CreateFile(handle.uid, handle.gid, params->path, params->attribute,
|
||||
params->owner_mode, params->group_mode, params->other_mode);
|
||||
LogResult(StringFromFormat("CreateFile(%s)", params->path), result);
|
||||
return GetFSReply(ConvertResult(result));
|
||||
return GetReplyForSuperblockOperation(result);
|
||||
}
|
||||
|
||||
IPCCommandResult FS::SetFileVersionControl(const Handle& handle, const IOCtlRequest& request)
|
||||
|
@ -46,6 +46,7 @@ private:
|
||||
IOS::HLE::FS::Fd fs_fd = INVALID_FD;
|
||||
// We use a std::array to keep this savestate friendly.
|
||||
std::array<char, 64> name{};
|
||||
bool superblock_flush_needed = false;
|
||||
};
|
||||
|
||||
enum
|
||||
@ -79,7 +80,15 @@ private:
|
||||
IPCCommandResult GetUsage(const Handle& handle, const IOCtlVRequest& request);
|
||||
IPCCommandResult Shutdown(const Handle& handle, const IOCtlRequest& request);
|
||||
|
||||
u64 EstimateTicksForReadWrite(const Handle& handle, const ReadWriteRequest& request);
|
||||
u64 SimulatePopulateFileCache(u32 fd, u32 offset, u32 file_size);
|
||||
u64 SimulateFlushFileCache();
|
||||
bool HasCacheForFile(u32 fd, u32 offset) const;
|
||||
|
||||
std::map<u32, Handle> m_fd_map;
|
||||
u32 m_cache_fd = INVALID_FD;
|
||||
u16 m_cache_chain_index = 0;
|
||||
bool m_dirty_cache = false;
|
||||
};
|
||||
} // namespace Device
|
||||
} // namespace HLE
|
||||
|
@ -74,7 +74,7 @@ static Common::Event g_compressAndDumpStateSyncEvent;
|
||||
static std::thread g_save_thread;
|
||||
|
||||
// Don't forget to increase this after doing changes on the savestate system
|
||||
static const u32 STATE_VERSION = 95; // Last changed in PR 6421
|
||||
static const u32 STATE_VERSION = 96; // Last changed in PR 6565
|
||||
|
||||
// Maps savestate versions to Dolphin versions.
|
||||
// Versions after 42 don't need to be added to this list,
|
||||
|
Loading…
Reference in New Issue
Block a user