diff --git a/FEXCore/Source/CMakeLists.txt b/FEXCore/Source/CMakeLists.txt index bfed4ad59..dd8e4aa24 100644 --- a/FEXCore/Source/CMakeLists.txt +++ b/FEXCore/Source/CMakeLists.txt @@ -262,6 +262,10 @@ endfunction() # Build FEXCore_Base static library add_library(FEXCore_Base STATIC ${FEXCORE_BASE_SRCS}) target_link_libraries(FEXCore_Base PUBLIC ${LIBS}) +if (MINGW) + target_link_libraries(FEXCore_Base PUBLIC ntdll) +endif() + AddDefaultOptionsToTarget(FEXCore_Base) if (ENABLE_FEXCORE_PROFILER AND FEXCORE_PROFILER_BACKEND STREQUAL "TRACY") diff --git a/FEXCore/Source/Interface/Config/Config.json.in b/FEXCore/Source/Interface/Config/Config.json.in index db567f5bb..938377f32 100644 --- a/FEXCore/Source/Interface/Config/Config.json.in +++ b/FEXCore/Source/Interface/Config/Config.json.in @@ -229,6 +229,15 @@ "Desc": [ "Size limit on the main cache file - index not included. Default 1G" ] + }, + "DiskCachePruneStaleEntries": { + "Type": "bool", + "Default": "true", + "AffectsCodeGen": "false", + "Desc": [ + "Allows FEX to prune stale disk cache entries on startup.", + "Frees space by removing cache entries associated with old FEX versions." + ] } }, "Emulation": { diff --git a/FEXCore/Source/Interface/Core/DiskCache.cpp b/FEXCore/Source/Interface/Core/DiskCache.cpp index 8f714800e..cf341913b 100644 --- a/FEXCore/Source/Interface/Core/DiskCache.cpp +++ b/FEXCore/Source/Interface/Core/DiskCache.cpp @@ -2,18 +2,22 @@ #define XXH_STATIC_LINKING_ONLY -#include "FEXCore/Utils/TypeDefines.h" #include "Interface/Core/Frontend.h" -#include "FEXCore/Config/Config.h" -#include "FEXCore/fextl/string.h" -#include "FEXHeaderUtils/Filesystem.h" -#include "FEXCore/Core/DiskCache.h" -#include "FEXCore/Core/DiskCacheFileMapper.h" -#include "FEXCore/Utils/LogManager.h" #include "Interface/Context/Context.h" -#include "FEXCore/HLE/SyscallHandler.h" -#include "FEXCore/Utils/File.h" -#include "FEXCore/fextl/memory.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include #include #include @@ -356,6 +360,39 @@ namespace DiskCache { FileMapper = Func; } + static inline void PruneStaleEntries(std::string_view CacheBase, std::string_view MachineBucketHash) { + FEXCORE_PROFILE_SCOPED("DiskCache::PruneStaleEntries"); + FEX_CONFIG_OPT(DiskCachePruneStaleEntries, DISKCACHEPRUNESTALEENTRIES); + + if (!DiskCachePruneStaleEntries) { + return; + } + + struct SimpleCapture { + std::string_view CacheBase, MachineBucketHash; + } const SimpleCapture { + .CacheBase = CacheBase, + .MachineBucketHash = MachineBucketHash, + }; + + FEXCore::FileUtils::WalkDirectory( + CacheBase, + [](std::string_view name, bool is_dir, const void* user_data) { + if (!is_dir) { + return; + } + + auto capture = reinterpret_cast(user_data); + + // Current behaviour is to remove entries that no longer match the MachineBucketHash. + // This means that if the Disk Cache version no longer matches, or the FEXCore::HostFeatures differ, then they get removed. + if (name != capture->MachineBucketHash) { + FEXCore::FileUtils::RecursiveRemoveDirectory(fextl::fmt::format("{}/{}", capture->CacheBase, name)); + } + }, + &SimpleCapture); + } + void DiskCache::Init(FEXCore::Context::ContextImpl* CTX) { this->CTX = CTX; @@ -365,18 +402,24 @@ namespace DiskCache { fextl::string SerializedConfig = FEXCore::Config::SerializeForCache(); + const auto HostFeatureHash = CTX->HostFeatures.HashForCaching(); + struct __attribute__((packed)) { uint16_t FormatVersion; uint64_t HostFeaturesHash; - } MachineBucketData = {FormatVersion, CTX->HostFeatures.HashForCaching()}; + } MachineBucketData = {FormatVersion, HostFeatureHash.HostFeaturesHash}; - fextl::vector BucketBytes(sizeof(MachineBucketData) + sizeof(uint8_t) + SerializedConfig.size()); + fextl::vector BucketBytes(sizeof(MachineBucketData) + (sizeof(uint8_t) * 2) + SerializedConfig.size()); memcpy(BucketBytes.data(), &MachineBucketData, sizeof(MachineBucketData)); + + // 64-bit mode and HostType is in the ProcessBucket hash only instead of the MachineBucketHash + // These effect code-gen, but they aren't part of the MachineBucketHash, as it comes from Process state. BucketBytes[sizeof(MachineBucketData)] = CTX->Config.Is64BitMode; - memcpy(BucketBytes.data() + sizeof(MachineBucketData) + 1, SerializedConfig.data(), SerializedConfig.size()); + BucketBytes[sizeof(MachineBucketData) + 1] = FEXCore::ToUnderlying(HostFeatureHash.HostType); + memcpy(BucketBytes.data() + sizeof(MachineBucketData) + 2, SerializedConfig.data(), SerializedConfig.size()); uint64_t MachineBucketHash = XXH3_64bits(BucketBytes.data(), sizeof(MachineBucketData)); - uint64_t ProcessBucketHash = XXH3_64bits(BucketBytes.data() + sizeof(MachineBucketData), 1 + SerializedConfig.size()); + uint64_t ProcessBucketHash = XXH3_64bits(BucketBytes.data() + sizeof(MachineBucketData), 2 + SerializedConfig.size()); BucketHash.high64 = MachineBucketHash; BucketHash.low64 = ProcessBucketHash; @@ -384,9 +427,12 @@ namespace DiskCache { if (BasePath.empty()) { BasePath = FEXCore::Config::GetCacheDirectory() + "DiskCache/"; } - BasePath += fextl::fmt::format("{:016x}", MachineBucketHash) + "/"; + + const auto MachineBucketHashAsString = fextl::fmt::format("{:016x}", MachineBucketHash); + PruneStaleEntries(BasePath, MachineBucketHashAsString); + + BasePath += MachineBucketHashAsString + "/"; FHU::Filesystem::CreateDirectories(BasePath); - // todo could kick off clean up of leftover MachineBucketHash sibling directories here if (!MapDiskCacheFiles) { FileMapper = nullptr; diff --git a/FEXCore/Source/Utils/FileUtils.cpp b/FEXCore/Source/Utils/FileUtils.cpp index 5d5c644e4..d3e62cacd 100644 --- a/FEXCore/Source/Utils/FileUtils.cpp +++ b/FEXCore/Source/Utils/FileUtils.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT #include +#include #include #include #include @@ -8,6 +9,18 @@ #include #include #include +#else +#include +#include +#include +#include + +// FEX today doesn't have Windows include in FEXCore. +extern "C" NTSTATUS WINAPI NtQueryDirectoryFile(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, PIO_STATUS_BLOCK, PVOID, ULONG, + FILE_INFORMATION_CLASS, BOOLEAN, PUNICODE_STRING, BOOLEAN); + +extern "C" NTSTATUS RtlUnicodeToUTF8N(OUT PCHAR UTF8StringDestination, IN ULONG UTF8StringMaxByteCount, + OUT PULONG UTF8StringActualByteCount, IN PCWCH UnicodeStringSource, IN ULONG UnicodeStringByteCount); #endif namespace FEXCore::FileUtils { @@ -115,10 +128,239 @@ end: FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(const fextl::string& Directory) { return RecursiveRemoveDirectory(AT_FDCWD, Directory.c_str()) && unlinkat(AT_FDCWD, Directory.c_str(), true); } -#else -FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(std::string_view Directory) { - std::error_code ec; - return std::filesystem::remove_all(Directory, ec) != ~0ULL; + +FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, + fextl::move_only_function Callback, + const void* user_data) { + constexpr int DIR_FLAGS = O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC; + int dir_fd = ::openat(AT_FDCWD, fextl::string(Directory).c_str(), DIR_FLAGS); + if (dir_fd == -1) { + if (errno == ENOENT) { + // Probably raced something. Non-error. + return; + } + } + // Four pages arbitrary chosen to be a balance between NFS wanting to return data in page-size granules and + // local filesystems returning arbitrary sizes. + size_t dirent_size = 4096 * 4; + uint8_t* dirent_buffer = reinterpret_cast(FEXCore::Allocator::malloc(dirent_size)); + + if (!dirent_buffer) { + // Ran out of memory? + goto end; + } + + while (true) { + ssize_t read = getdents64(dir_fd, dirent_buffer, dirent_size); + + if (read == -1) { + if (errno == EINVAL) { + // Buffer too small? Scale and try again. + dirent_size *= 2; + FEXCore::Allocator::free(dirent_buffer); + dirent_buffer = reinterpret_cast(FEXCore::Allocator::malloc(dirent_size)); + if (!dirent_buffer) { + // Ran out of memory? + goto end; + } + + continue; + } + + // Anything else just exit. + goto end; + } + + if (read == 0) { + // Done. + break; + } + + for (size_t dirent_offset = 0; dirent_offset < read;) { + auto path_dirent = reinterpret_cast(dirent_buffer + dirent_offset); + std::string_view path_name_view = path_dirent->d_name; + + if (path_name_view == "." || path_name_view == "..") { + // Skip these two special files. + dirent_offset += path_dirent->d_reclen; + continue; + } + + Callback(path_name_view, path_dirent->d_type == DT_DIR, user_data); + + // dirent is a VLA so we need to increment by reported size. + dirent_offset += path_dirent->d_reclen; + } + } + +end: + if (dirent_buffer) { + FEXCore::Allocator::free(dirent_buffer); + } + + close(dir_fd); } + +#else +static inline std::optional PathToNTPath(std::string_view Path) { + UNICODE_STRING PathW; + if (!RtlCreateUnicodeStringFromAsciiz(&PathW, fextl::string(Path).c_str())) { + return std::nullopt; + } + UNICODE_STRING NTPath; + bool Success = RtlDosPathNameToNtPathName_U(PathW.Buffer, &NTPath, nullptr, nullptr); + RtlFreeUnicodeString(&PathW); + if (!Success) { + return std::nullopt; + } + + return NTPath; +} + +static inline void FreeNTPath(UNICODE_STRING Path) { + RtlFreeUnicodeString(&Path); +} + +FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, + fextl::move_only_function Callback, + const void* user_data) { + auto NTPath = PathToNTPath(Directory); + if (!NTPath) { + return; + } + + NTSTATUS Status {}; + + HANDLE dir_fd {}; + OBJECT_ATTRIBUTES attr {}; + IO_STATUS_BLOCK io {}; + + InitializeObjectAttributes(&attr, &*NTPath, OBJ_CASE_INSENSITIVE, nullptr, nullptr); + Status = NtOpenFile(&dir_fd, FILE_LIST_DIRECTORY | SYNCHRONIZE, &attr, &io, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); + + FreeNTPath(*NTPath); + + if (!NT_SUCCESS(Status)) { + return; + } + + BOOLEAN FirstQuery = TRUE; + size_t dirent_size = 4096 * 4; + uint8_t* dirent_buffer = reinterpret_cast(FEXCore::Allocator::malloc(dirent_size)); + + if (!dirent_buffer) { + // Ran out of memory? + goto end; + } + + while (true) { + Status = NtQueryDirectoryFile(dir_fd, + nullptr, // Event + nullptr, // ApcRoutine + nullptr, // ApcContext + &io, dirent_buffer, dirent_size, FileDirectoryInformation, + FALSE, // ReturnSingleEntry + nullptr, // FileName + FirstQuery); + FirstQuery = FALSE; + + if (Status == STATUS_NO_MORE_FILES) { + // No more files + break; + } + + if (!NT_SUCCESS(Status)) { + if (Status == STATUS_BUFFER_TOO_SMALL || Status == STATUS_INFO_LENGTH_MISMATCH) { + // Buffer too small? Scale and try again. + dirent_size *= 2; + FEXCore::Allocator::free(dirent_buffer); + uint8_t* dirent_buffer = reinterpret_cast(FEXCore::Allocator::malloc(dirent_size)); + + if (!dirent_buffer) { + // Ran out of memory? + goto end; + } + + continue; + } + + // Any other failure, exit loop. + break; + } + + // Iterate the entries returned. + for (size_t dirent_offset = 0;;) { + auto Info = reinterpret_cast(dirent_buffer + dirent_offset); + + std::wstring_view EntryName(Info->FileName, Info->FileNameLength / sizeof(wchar_t)); + + if (EntryName != L"." && EntryName != L"..") { + ULONG utf8_bytes_needed = 0; + ULONG actual_utf8_bytes = 0; + RtlUnicodeToUTF8N(nullptr, 0, &utf8_bytes_needed, Info->FileName, Info->FileNameLength); + char* dynamic_buf = reinterpret_cast(FEXCore::Allocator::malloc(utf8_bytes_needed + 1)); + if (dynamic_buf) { + RtlUnicodeToUTF8N(dynamic_buf, utf8_bytes_needed + 1, &actual_utf8_bytes, Info->FileName, Info->FileNameLength); + std::string_view name_view(dynamic_buf, actual_utf8_bytes); + bool is_dir = (Info->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + Callback(name_view, is_dir, user_data); + FEXCore::Allocator::free(dynamic_buf); + } + } + + if (Info->NextEntryOffset == 0) { + break; + } + + dirent_offset += Info->NextEntryOffset; + } + } + +end: + if (dirent_buffer) { + FEXCore::Allocator::free(dirent_buffer); + } + + NtClose(dir_fd); +} + +FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(const fextl::string& Directory) { + using CallbackType = void (*)(std::string_view name, bool is_dir, const void* user_data); + struct UserData { + CallbackType RemoveFile {}; + std::string_view base_path; + }; + + CallbackType RemoveFile = [](std::string_view name, bool is_dir, const void* user_data) { + auto Data = reinterpret_cast(user_data); + + auto full_path = std::format("{}/{}", Data->base_path, name); + if (is_dir) { + UserData NewData { + .RemoveFile = Data->RemoveFile, + .base_path = full_path, + }; + + WalkDirectory(full_path, Data->RemoveFile, &NewData); + } + + if (is_dir) { + RemoveDirectoryA(full_path.c_str()); + } else { + DeleteFileA(full_path.c_str()); + } + }; + + UserData Data { + .RemoveFile = RemoveFile, + .base_path = Directory, + }; + + WalkDirectory(Directory, RemoveFile, &Data); + + return RemoveDirectoryA(Directory.c_str()) != 0; +} + #endif } // namespace FEXCore::FileUtils diff --git a/FEXCore/include/FEXCore/Core/HostFeatures.h b/FEXCore/include/FEXCore/Core/HostFeatures.h index 71e910eb1..9708cd968 100644 --- a/FEXCore/include/FEXCore/Core/HostFeatures.h +++ b/FEXCore/include/FEXCore/Core/HostFeatures.h @@ -32,16 +32,23 @@ struct HostFeatures { return 4 << DCacheLineLog2; } + struct CacheHash { + uint64_t HostFeaturesHash; + HostTypeEnum HostType; + }; + [[nodiscard]] - uint64_t HashForCaching() const { - // As long as the number of options is 64-bit or below, we can just return it. - // Skip CPUMIDRs as it doesn't affect codegen. - static_assert(offsetof(HostFeatures, CPUMIDRs) == 8); - uint64_t Result {}; - memcpy(&Result, this, sizeof(Result)); + CacheHash HashForCaching() const { + static_assert(offsetof(HostFeatures, HostType) == 8); + + CacheHash Result {}; + memcpy(&Result.HostFeaturesHash, this, sizeof(uint64_t)); + Result.HostType = HostType; + return Result; } + // Affects codegen and is basically machine description. uint32_t DCacheLineLog2 : 4 {}; uint32_t SupportsCacheMaintenanceOps : 1 {}; uint32_t SupportsAES : 1 {}; @@ -78,8 +85,10 @@ struct HostFeatures { uint32_t SupportsFloatExceptions : 1 {}; // Flag if this is InstCountCI uint32_t IsInstCountCI : 1 {}; - HostTypeEnum HostType : 2 {}; - uint32_t pad : 24 {}; + uint32_t pad : 26 {}; + + // This affects codegen, but it isn't machine state + HostTypeEnum HostType {}; // MIDR information // Also used for determining number of CPU cores for CPUID diff --git a/FEXCore/include/FEXCore/Utils/FileUtils.h b/FEXCore/include/FEXCore/Utils/FileUtils.h index d64241dae..07365aa92 100644 --- a/FEXCore/include/FEXCore/Utils/FileUtils.h +++ b/FEXCore/include/FEXCore/Utils/FileUtils.h @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT #pragma once +#include #include + #include namespace FEXCore::FileUtils { @@ -10,4 +12,8 @@ namespace FEXCore::FileUtils { * @return True if the directory didn't exist or was deleted. */ FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(const fextl::string& Directory); + +FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, + fextl::move_only_function Callback, + const void* user_data); } // namespace FEXCore::FileUtils diff --git a/Source/Windows/Common/WinAPI/IO.cpp b/Source/Windows/Common/WinAPI/IO.cpp index ac960554a..27208430b 100644 --- a/Source/Windows/Common/WinAPI/IO.cpp +++ b/Source/Windows/Common/WinAPI/IO.cpp @@ -45,6 +45,37 @@ FILE_INFORMATION_CLASS FileInfoClassToNT(FILE_INFO_BY_HANDLE_CLASS InformationCl } } // namespace +DLLEXPORT_FUNC(BOOL, RemoveDirectoryA, (LPCSTR lpFileName)) { + ScopedUnicodeString FileName {lpFileName}; + return RemoveDirectoryW(FileName->Buffer); +} + +DLLEXPORT_FUNC(BOOL, RemoveDirectoryW, (LPCWSTR lpFileName)) { + UNICODE_STRING PathW; + RtlInitUnicodeString(&PathW, lpFileName); + + ScopedUnicodeString NTPath; + if (!RtlDosPathNameToNtPathName_U(PathW.Buffer, &*NTPath, nullptr, nullptr)) { + SetLastError(ERROR_PATH_NOT_FOUND); + return false; + } + + OBJECT_ATTRIBUTES ObjAttributes; + InitializeObjectAttributes(&ObjAttributes, &*NTPath, OBJ_CASE_INSENSITIVE, nullptr, nullptr); + + HANDLE Handle; + IO_STATUS_BLOCK IOSB; + + NTSTATUS Status = + NtCreateFile(&Handle, SYNCHRONIZE | DELETE, &ObjAttributes, &IOSB, nullptr, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, FILE_DELETE_ON_CLOSE | FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT, nullptr, 0); + if (WinAPIReturn(Status)) { + Status = NtClose(Handle); + } + + return WinAPIReturn(Status); +} + DLLEXPORT_FUNC(BOOL, DeleteFileA, (LPCSTR lpFileName)) { ScopedUnicodeString FileName {lpFileName}; return DeleteFileW(FileName->Buffer);