diff options
| author | Jordan Woyak <jordan.woyak@gmail.com> | 2025-11-10 17:29:43 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-10 17:29:43 -0600 |
| commit | f51eaf3282416157af1f58f4803c683dcae7e70f (patch) | |
| tree | 1218824f42541a80591317b3b629923b007d111a /Source/Core/Common/DirectIOFile.cpp | |
| parent | cd4902f0ed68ffac9f1e4ab853f37eefa34e6ae2 (diff) | |
| parent | 9e2fc7f4ddba14b1e7123d1654b385b21c71c744 (diff) | |
Merge pull request #14052 from jordan-woyak/DirectIOFile
Common and DiscIO: Introduce a thread safe DirectIOFile and make BlobReader implementations use it.
Diffstat (limited to 'Source/Core/Common/DirectIOFile.cpp')
| -rw-r--r-- | Source/Core/Common/DirectIOFile.cpp | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/Source/Core/Common/DirectIOFile.cpp b/Source/Core/Common/DirectIOFile.cpp new file mode 100644 index 0000000000..d5e805529d --- /dev/null +++ b/Source/Core/Common/DirectIOFile.cpp @@ -0,0 +1,372 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Common/DirectIOFile.h" + +#include <utility> + +#if defined(_WIN32) +#include <windows.h> + +#include <cstring> + +#include "Common/Buffer.h" +#include "Common/CommonFuncs.h" +#include "Common/MathUtil.h" +#include "Common/StringUtil.h" +#else +#include <string> + +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> + +#ifdef ANDROID +#include "jni/AndroidCommon/AndroidCommon.h" + +#include "Common/Lazy.h" +#endif + +#include "Common/FileUtil.h" +#endif + +#include "Common/Assert.h" + +namespace File +{ +DirectIOFile::DirectIOFile() = default; + +DirectIOFile::~DirectIOFile() +{ + Close(); +} + +DirectIOFile::DirectIOFile(const DirectIOFile& other) +{ + *this = other.Duplicate(); +} + +DirectIOFile& DirectIOFile::operator=(const DirectIOFile& other) +{ + return *this = other.Duplicate(); +} + +DirectIOFile::DirectIOFile(DirectIOFile&& other) +{ + Swap(other); +} + +DirectIOFile& DirectIOFile::operator=(DirectIOFile&& other) +{ + Close(); + Swap(other); + return *this; +} + +DirectIOFile::DirectIOFile(const std::string& path, AccessMode access_mode, OpenMode open_mode) +{ + Open(path, access_mode, open_mode); +} + +bool DirectIOFile::Open(const std::string& path, AccessMode access_mode, OpenMode open_mode) +{ + ASSERT(!IsOpen()); + + if (open_mode == OpenMode::Default) + open_mode = (access_mode == AccessMode::Write) ? OpenMode::Truncate : OpenMode::Existing; + + // This is not a sensible combination. Fail here to not rely on OS-specific behaviors. + if (access_mode == AccessMode::Read && open_mode == OpenMode::Truncate) + return false; + +#if defined(_WIN32) + DWORD desired_access = GENERIC_READ | GENERIC_WRITE; + if (access_mode == AccessMode::Read) + desired_access = GENERIC_READ; + else if (access_mode == AccessMode::Write) + desired_access = GENERIC_WRITE; + + // Allow deleting and renaming through our handle. + desired_access |= DELETE; + + // All sharing is allowed to more closely match default behavior on other OSes. + constexpr DWORD share_mode = FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE; + + DWORD creation_disposition = OPEN_ALWAYS; + if (open_mode == OpenMode::Truncate) + creation_disposition = CREATE_ALWAYS; + else if (open_mode == OpenMode::Create) + creation_disposition = CREATE_NEW; + else if (open_mode == OpenMode::Existing) + creation_disposition = OPEN_EXISTING; + + m_handle = CreateFile(UTF8ToTStr(path).c_str(), desired_access, share_mode, nullptr, + creation_disposition, FILE_ATTRIBUTE_NORMAL, nullptr); + if (!IsOpen()) + WARN_LOG_FMT(COMMON, "CreateFile: {}", Common::GetLastErrorString()); + +#else +#if defined(ANDROID) + if (IsPathAndroidContent(path)) + { + // Android documentation says that "w" may or may not truncate. + // In case it does, we'll use "rw" when we don't want truncation. + // This wrongly enables read access when we don't need it. + if (access_mode == AccessMode::Write && open_mode != OpenMode::Truncate) + access_mode = AccessMode::ReadAndWrite; + + std::string open_mode_str = "rw"; + if (access_mode == AccessMode::Read) + open_mode_str = "r"; + else if (access_mode == AccessMode::Write) + open_mode_str = "w"; + + // FYI: File::Exists can be slow on Android. + Common::Lazy<bool> file_exists{[&] { return Exists(path); }}; + + // A few features are emulated in a non-atomic manner. + if (open_mode == OpenMode::Existing) + { + if (access_mode != AccessMode::Read && !*file_exists) + return false; + } + else + { + if (open_mode == OpenMode::Truncate) + open_mode_str += 't'; + else if (open_mode == OpenMode::Create && *file_exists) + return false; + + // Modes other than `Existing` may create a file, but "r" won't do that automatically. + if (access_mode == AccessMode::Read && !*file_exists) + CreateEmptyFile(path); + } + + m_fd = OpenAndroidContent(path, open_mode_str); + + return IsOpen(); + } +#endif + int flags = O_RDWR; + if (access_mode == AccessMode::Read) + flags = O_RDONLY; + else if (access_mode == AccessMode::Write) + flags = O_WRONLY; + + if (open_mode == OpenMode::Truncate) + flags |= O_CREAT | O_TRUNC; + else if (open_mode == OpenMode::Create) + flags |= O_CREAT | O_EXCL; + else if (open_mode != OpenMode::Existing) + flags |= O_CREAT; + + m_fd = open(path.c_str(), flags, 0666); + +#endif + + return IsOpen(); +} + +bool DirectIOFile::Close() +{ + if (!IsOpen()) + return false; + + m_current_offset = 0; + +#if defined(_WIN32) + return CloseHandle(std::exchange(m_handle, INVALID_HANDLE_VALUE)) != 0; +#else + return close(std::exchange(m_fd, -1)) == 0; +#endif +} + +bool DirectIOFile::IsOpen() const +{ +#if defined(_WIN32) + return m_handle != INVALID_HANDLE_VALUE; +#else + return m_fd != -1; +#endif +} + +#if defined(_WIN32) +template <auto* TransferFunc> +static bool OverlappedTransfer(HANDLE handle, u64 offset, auto* data_ptr, u64 size) +{ + // ReadFile/WriteFile take a 32bit size so we must loop to handle our 64bit size. + while (true) + { + OVERLAPPED overlapped{}; + overlapped.Offset = DWORD(offset); + overlapped.OffsetHigh = DWORD(offset >> 32); + + DWORD bytes_transferred{}; + if (TransferFunc(handle, data_ptr, MathUtil::SaturatingCast<DWORD>(size), &bytes_transferred, + &overlapped) == 0) + { + ERROR_LOG_FMT(COMMON, "OverlappedTransfer: {}", Common::GetLastErrorString()); + return false; + } + + size -= bytes_transferred; + + if (size == 0) + return true; + + offset += bytes_transferred; + data_ptr += bytes_transferred; + } +} +#endif + +bool DirectIOFile::OffsetRead(u64 offset, u8* out_ptr, u64 size) +{ +#if defined(_WIN32) + return OverlappedTransfer<ReadFile>(m_handle, offset, out_ptr, size); +#else + return pread(m_fd, out_ptr, size, off_t(offset)) == ssize_t(size); +#endif +} + +bool DirectIOFile::OffsetWrite(u64 offset, const u8* in_ptr, u64 size) +{ +#if defined(_WIN32) + return OverlappedTransfer<WriteFile>(m_handle, offset, in_ptr, size); +#else + return pwrite(m_fd, in_ptr, size, off_t(offset)) == ssize_t(size); +#endif +} + +u64 DirectIOFile::GetSize() const +{ +#if defined(_WIN32) + LARGE_INTEGER result{}; + if (GetFileSizeEx(m_handle, &result) != 0) + return result.QuadPart; +#else + struct stat st{}; + if (fstat(m_fd, &st) == 0) + return st.st_size; +#endif + + return 0; +} + +bool DirectIOFile::Seek(s64 offset, SeekOrigin origin) +{ + if (!IsOpen()) + return false; + + u64 reference_pos = 0; + switch (origin) + { + case SeekOrigin::Current: + reference_pos = m_current_offset; + break; + case SeekOrigin::End: + reference_pos = GetSize(); + break; + default: + break; + } + + // Don't let our current offset underflow. + if (offset < 0 && u64(-offset) > reference_pos) + return false; + + m_current_offset = reference_pos + offset; + return true; +} + +bool DirectIOFile::Flush() +{ +#if defined(_WIN32) + return FlushFileBuffers(m_handle) != 0; +#else + return fsync(m_fd) == 0; +#endif +} + +void DirectIOFile::Swap(DirectIOFile& other) +{ +#if defined(_WIN32) + std::swap(m_handle, other.m_handle); +#else + std::swap(m_fd, other.m_fd); +#endif + std::swap(m_current_offset, other.m_current_offset); +} + +DirectIOFile DirectIOFile::Duplicate() const +{ + DirectIOFile result; + + if (!IsOpen()) + return result; + +#if defined(_WIN32) + const auto current_process = GetCurrentProcess(); + if (DuplicateHandle(current_process, m_handle, current_process, &result.m_handle, 0, FALSE, + DUPLICATE_SAME_ACCESS) == 0) + { + ERROR_LOG_FMT(COMMON, "DuplicateHandle: {}", Common::GetLastErrorString()); + } +#else + result.m_fd = dup(m_fd); +#endif + + ASSERT(result.IsOpen()); + + result.m_current_offset = m_current_offset; + + return result; +} + +bool Resize(DirectIOFile& file, u64 size) +{ +#if defined(_WIN32) + // This operation is not "atomic", but it's the only thing we're using the file pointer for. + // Concurrent `Resize` would need some external synchronization to prevent race regardless. + const LARGE_INTEGER distance{.QuadPart = LONGLONG(size)}; + return (SetFilePointerEx(file.GetHandle(), distance, nullptr, FILE_BEGIN) != 0) && + (SetEndOfFile(file.GetHandle()) != 0); +#else + return ftruncate(file.GetHandle(), off_t(size)) == 0; +#endif +} + +bool Rename(DirectIOFile& file, const std::string& source_path [[maybe_unused]], + const std::string& destination_path) +{ +#if defined(_WIN32) + const auto dest_name = UTF8ToWString(destination_path); + const auto dest_name_byte_size = DWORD(dest_name.size() * sizeof(WCHAR)); + FILE_RENAME_INFO info{ + .ReplaceIfExists = TRUE, + .FileNameLength = dest_name_byte_size, // The size in bytes, not including null termination. + }; + constexpr auto filename_struct_offset = offsetof(FILE_RENAME_INFO, FileName); + Common::UniqueBuffer<u8> buffer(filename_struct_offset + dest_name_byte_size + sizeof(WCHAR)); + std::memcpy(buffer.data(), &info, filename_struct_offset); + std::memcpy(buffer.data() + filename_struct_offset, dest_name.c_str(), + dest_name_byte_size + sizeof(WCHAR)); + return SetFileInformationByHandle(file.GetHandle(), FileRenameInfo, buffer.data(), + DWORD(buffer.size())) != 0; +#else + return file.IsOpen() && Rename(source_path, destination_path); +#endif +} + +bool Delete(DirectIOFile& file, const std::string& filename) +{ +#if defined(_WIN32) + FILE_DISPOSITION_INFO info{.DeleteFile = TRUE}; + return SetFileInformationByHandle(file.GetHandle(), FileDispositionInfo, &info, sizeof(info)) != + 0; +#else + return file.IsOpen() && Delete(filename, IfAbsentBehavior::NoConsoleWarning); +#endif +} + +} // namespace File |
