summaryrefslogtreecommitdiff
path: root/Source/Core/Common/DirectIOFile.cpp
diff options
context:
space:
mode:
authorJordan Woyak <jordan.woyak@gmail.com>2025-11-10 17:29:43 -0600
committerGitHub <noreply@github.com>2025-11-10 17:29:43 -0600
commitf51eaf3282416157af1f58f4803c683dcae7e70f (patch)
tree1218824f42541a80591317b3b629923b007d111a /Source/Core/Common/DirectIOFile.cpp
parentcd4902f0ed68ffac9f1e4ab853f37eefa34e6ae2 (diff)
parent9e2fc7f4ddba14b1e7123d1654b385b21c71c744 (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.cpp372
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