From 47bc73575e8d61d04950c4347570af2d970bd09a Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Thu, 24 Sep 2026 14:53:53 -0700 Subject: [PATCH 1/4] FEXCore/FileUtils: Adds a way to walk a directory with callback We need to walk a directory calling back to the DiskCache, but without recursion. Also fixes a function signature issue on win32. --- FEXCore/Source/Utils/FileUtils.cpp | 80 ++++++++++++++++++++++- FEXCore/include/FEXCore/Utils/FileUtils.h | 4 ++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/FEXCore/Source/Utils/FileUtils.cpp b/FEXCore/Source/Utils/FileUtils.cpp index 5d5c644e4..7f69ddada 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 @@ -115,10 +116,87 @@ end: FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(const fextl::string& Directory) { return RecursiveRemoveDirectory(AT_FDCWD, Directory.c_str()) && unlinkat(AT_FDCWD, Directory.c_str(), true); } + +FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, fextl::move_only_function Callback) { + 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); + + // 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 -FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(std::string_view Directory) { +FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(const fextl::string& 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) { + for (const auto& iter : std::filesystem::directory_iterator(Directory)) { + Callback(iter.path().string(), iter.is_directory()); + } +} #endif } // namespace FEXCore::FileUtils diff --git a/FEXCore/include/FEXCore/Utils/FileUtils.h b/FEXCore/include/FEXCore/Utils/FileUtils.h index d64241dae..eb6aa5077 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,6 @@ 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); } // namespace FEXCore::FileUtils From 47557a6184f501fd283e2e5c10298f3010fca47d Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Thu, 24 Sep 2026 14:55:42 -0700 Subject: [PATCH 2/4] FEXCore/DiskCache: Prune old entries automatically Adds a config option to disable pruning if desired. Had to change FEXCore::HostFeatures slightly to remove the HostType from the Hash for caching, as we don't want the bucket to inadvertently delete wow64,arm64ec,linux buckets when executing in the same namespace. Minor changes to the DiskCache::Init code to handle returning two values, moving the HostType to be a part of the "ProcessBucketHash" instead of the "MachineBucketHash". Then uses the directory walking function added in the previous commit to find mismatched/old "MachineBucketHash" directories and remove them. --- .../Source/Interface/Config/Config.json.in | 9 +++ FEXCore/Source/Interface/Core/DiskCache.cpp | 78 +++++++++++++++---- FEXCore/Source/Utils/FileUtils.cpp | 12 ++- FEXCore/include/FEXCore/Core/HostFeatures.h | 25 ++++-- FEXCore/include/FEXCore/Utils/FileUtils.h | 4 +- 5 files changed, 99 insertions(+), 29 deletions(-) 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 7f69ddada..1de611a02 100644 --- a/FEXCore/Source/Utils/FileUtils.cpp +++ b/FEXCore/Source/Utils/FileUtils.cpp @@ -117,7 +117,9 @@ FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(const fextl::string& Direct return RecursiveRemoveDirectory(AT_FDCWD, Directory.c_str()) && unlinkat(AT_FDCWD, Directory.c_str(), true); } -FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, fextl::move_only_function Callback) { +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) { @@ -172,7 +174,7 @@ FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, fextl::mov continue; } - Callback(path_name_view, path_dirent->d_type == DT_DIR); + 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; @@ -193,9 +195,11 @@ FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(const fextl::string& Direct return std::filesystem::remove_all(Directory, ec) != ~0ULL; } -FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, fextl::move_only_function Callback) { +FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, + fextl::move_only_function Callback, + const void* user_data) { for (const auto& iter : std::filesystem::directory_iterator(Directory)) { - Callback(iter.path().string(), iter.is_directory()); + Callback(iter.path().string(), iter.is_directory(), user_data); } } #endif 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 eb6aa5077..07365aa92 100644 --- a/FEXCore/include/FEXCore/Utils/FileUtils.h +++ b/FEXCore/include/FEXCore/Utils/FileUtils.h @@ -13,5 +13,7 @@ namespace FEXCore::FileUtils { */ FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(const fextl::string& Directory); -FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, fextl::move_only_function Callback); +FEX_DEFAULT_VISIBILITY void WalkDirectory(std::string_view Directory, + fextl::move_only_function Callback, + const void* user_data); } // namespace FEXCore::FileUtils From f3b0b412548e45843470ef2b3bef1af0f26ea0f3 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Fri, 25 Sep 2026 14:39:55 -0700 Subject: [PATCH 3/4] Win32: Implement RemoveDirectory --- Source/Windows/Common/WinAPI/IO.cpp | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) 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); From 7962f4006565272b7b0c8c1ae1536a2a3d1a0e22 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Fri, 25 Sep 2026 14:40:21 -0700 Subject: [PATCH 4/4] FileUtils: Implement RecursiveRemoveDirectory and WalkDirectory with NT syscalls A bit messy but it basically duplicates the Linux side. We don't have a way to include NT internal defines in to FEXCore currently so the two required symbols are just forward declared. --- FEXCore/Source/CMakeLists.txt | 4 + FEXCore/Source/Utils/FileUtils.cpp | 170 ++++++++++++++++++++++++++++- 2 files changed, 169 insertions(+), 5 deletions(-) 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/Utils/FileUtils.cpp b/FEXCore/Source/Utils/FileUtils.cpp index 1de611a02..d3e62cacd 100644 --- a/FEXCore/Source/Utils/FileUtils.cpp +++ b/FEXCore/Source/Utils/FileUtils.cpp @@ -9,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 { @@ -190,17 +202,165 @@ end: } #else -FEX_DEFAULT_VISIBILITY bool RecursiveRemoveDirectory(const fextl::string& Directory) { - std::error_code ec; - return std::filesystem::remove_all(Directory, ec) != ~0ULL; +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) { - for (const auto& iter : std::filesystem::directory_iterator(Directory)) { - Callback(iter.path().string(), iter.is_directory(), 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