diff --git a/Source/Core/Core/IOS/FS/FileSystemProxy.cpp b/Source/Core/Core/IOS/FS/FileSystemProxy.cpp index 75d0c6c6b4..fbabef74cb 100644 --- a/Source/Core/Core/IOS/FS/FileSystemProxy.cpp +++ b/Source/Core/Core/IOS/FS/FileSystemProxy.cpp @@ -4,6 +4,7 @@ #include "Core/IOS/FS/FileSystemProxy.h" +#include #include #include "Common/Assert.h" @@ -29,12 +30,20 @@ static s32 ConvertResult(ResultCode code) return -(static_cast(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& 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(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(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 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(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(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 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 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 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 = 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) diff --git a/Source/Core/Core/IOS/FS/FileSystemProxy.h b/Source/Core/Core/IOS/FS/FileSystemProxy.h index c14f77f049..22683e191e 100644 --- a/Source/Core/Core/IOS/FS/FileSystemProxy.h +++ b/Source/Core/Core/IOS/FS/FileSystemProxy.h @@ -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 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 m_fd_map; + u32 m_cache_fd = INVALID_FD; + u16 m_cache_chain_index = 0; + bool m_dirty_cache = false; }; } // namespace Device } // namespace HLE diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 8956763c0f..925039f5da 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -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,