From 6db5bc372129873a04b10e0d59962dbc4c684520 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Mon, 11 Aug 2025 16:08:27 -0700 Subject: [PATCH 1/3] FEXCore/Utils/IntervalList: Adds a couple functions --- FEXCore/include/FEXCore/Utils/IntervalList.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/FEXCore/include/FEXCore/Utils/IntervalList.h b/FEXCore/include/FEXCore/Utils/IntervalList.h index 80dee2ede..5535b0a49 100644 --- a/FEXCore/include/FEXCore/Utils/IntervalList.h +++ b/FEXCore/include/FEXCore/Utils/IntervalList.h @@ -34,10 +34,23 @@ public: Interval Interval; ///< The interval that the query offset is enclosed by, or the next interval if `Enclosed` is false }; + using const_iterator = typename fextl::vector::const_iterator; + + const_iterator begin() const { + return Intervals.begin(); + } + const_iterator end() const { + return Intervals.end(); + } + void Clear() { Intervals.clear(); } + bool Empty() const { + return Intervals.empty(); + } + void Insert(Interval Entry) { if (Entry.Offset == Entry.End) { return; From f050571696f4bd547b28e4527acfcdf1e545f179 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Mon, 11 Aug 2025 16:09:33 -0700 Subject: [PATCH 2/3] Implement support for additional provided volatile metadata Only wired up for wow64 and arm64ec. Gives more granular control over TSO enabling and disabling. Matches arm64ec volatile metadata except with one more additional feature that whole modules can be disabled at a time. Once we know the mapped size of files in Linux then we'll be able to do the same thing there, but there's not a full mechanism wired up for that yet. --- .../Source/Interface/Config/Config.json.in | 21 +++ Source/Common/CMakeLists.txt | 4 +- Source/Common/VolatileMetadata.cpp | 120 ++++++++++++++++++ Source/Common/VolatileMetadata.h | 16 +++ Source/Windows/ARM64EC/Module.cpp | 39 ++++-- Source/Windows/Common/VolatileMetadata.h | 42 ++++++ Source/Windows/WOW64/Module.cpp | 40 +++++- 7 files changed, 269 insertions(+), 13 deletions(-) create mode 100644 Source/Common/VolatileMetadata.cpp create mode 100644 Source/Common/VolatileMetadata.h create mode 100644 Source/Windows/Common/VolatileMetadata.h diff --git a/FEXCore/Source/Interface/Config/Config.json.in b/FEXCore/Source/Interface/Config/Config.json.in index d1045decc..131755b47 100644 --- a/FEXCore/Source/Interface/Config/Config.json.in +++ b/FEXCore/Source/Interface/Config/Config.json.in @@ -522,6 +522,27 @@ "Desc": [ "Disables inline syscalls in order to support seccomp handling" ] + }, + "ExtendedVolatileMetadata": { + "Type": "str", + "Default": "", + "Desc": [ + "Configuration provided volatile metadata. Only implemented for WoW64/arm64ec.", + "Limited in its use but can be handy.", + "Extends on top of what Microsoft has for volatile metadata, but also supported for WoW64.", + "Colon delimited modules, then semi-colon delimited instructions, then comma delimited ranges", + "Default disables TSO in the module, unless instructions overlap the range", + ";-,...;,...:", + "examples:", + " * Disable TSO for a full module: Just provide the module name:", + " `hl2_linux`", + " * Disable TSO for a part of the module:", + " `hl2_linux;-`", + " * Disable TSO for a part of the module, but enable TSO for some instructions within the module", + " `hl2_linux;-;,`", + " * Disable TSO for multiple modules", + " `hl2_linux:libsdl2.so`" + ] } } }, diff --git a/Source/Common/CMakeLists.txt b/Source/Common/CMakeLists.txt index 822a69ac5..95cd7feb7 100644 --- a/Source/Common/CMakeLists.txt +++ b/Source/Common/CMakeLists.txt @@ -9,7 +9,9 @@ set(SRCS HostFeatures.cpp JSONPool.cpp StringUtil.cpp - SHMStats.cpp) + SHMStats.cpp + VolatileMetadata.cpp + ) if (NOT MINGW_BUILD) list (APPEND SRCS diff --git a/Source/Common/VolatileMetadata.cpp b/Source/Common/VolatileMetadata.cpp new file mode 100644 index 000000000..90ace901f --- /dev/null +++ b/Source/Common/VolatileMetadata.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +#include "Common/VolatileMetadata.h" + +namespace FEX::VolatileMetadata { +fextl::unordered_map ParseExtendedVolatileMetadata(std::string_view ListOfDescriptors) { + // Parsing: `;
-,;:` + if (ListOfDescriptors.empty()) { + return {}; + } + + fextl::unordered_map ExtendedMetaData {}; + + auto current_module = ExtendedMetaData.end(); + + for (size_t module_offset = 0; module_offset != ListOfDescriptors.npos;) { + size_t end_of_module = ListOfDescriptors.find(":", module_offset); + std::string_view module_config = ListOfDescriptors.substr(module_offset, end_of_module - module_offset); + + if (module_config.empty()) { + module_offset = end_of_module == ListOfDescriptors.npos ? ListOfDescriptors.npos : end_of_module + 1; + continue; + } + + size_t end_of_name = module_config.find(";"); + size_t end_of_address_ranges = module_config.npos; + size_t end_of_individual_inst = module_config.npos; + + // Module name handling + { + std::string_view section_str = module_config.substr(0, end_of_name); + + if (section_str.empty()) { + module_offset = end_of_module == ListOfDescriptors.npos ? ListOfDescriptors.npos : end_of_module + 1; + continue; + } + + current_module = ExtendedMetaData + .insert_or_assign(fextl::string(section_str), + ExtendedVolatileMetadata { + .ModuleTSODisabled = true, + }) + .first; + } + + // Address range handling + if (end_of_name != module_config.npos) { + end_of_address_ranges = module_config.find(";", end_of_name + 1); + std::string_view section_str = module_config.substr(end_of_name + 1, end_of_address_ranges - (end_of_name + 1)); + + if (section_str.empty()) { + module_offset = end_of_module == ListOfDescriptors.npos ? ListOfDescriptors.npos : end_of_module + 1; + continue; + } + + current_module->second.ModuleTSODisabled = false; + + // Walk all the address ranges provided. + for (size_t non_tso_region_offset = 0; non_tso_region_offset != section_str.npos;) { + size_t end_of_region_substr = section_str.find(",", non_tso_region_offset); + std::string_view tso_region_view = section_str.substr(non_tso_region_offset, end_of_region_substr - non_tso_region_offset); + + if (tso_region_view.empty()) { + non_tso_region_offset = end_of_region_substr == section_str.npos ? section_str.npos : end_of_region_substr + 1; + continue; + } + + uint64_t begin {}, end {}; + char* str_end; + begin = std::strtoull(tso_region_view.data(), &str_end, 16); + LOGMAN_THROW_A_FMT(tso_region_view.data() != str_end, "Couldn't parse begin {}", tso_region_view); + + // Skip `-` separator. + ++str_end; + + LOGMAN_THROW_A_FMT(str_end != tso_region_view.end(), "Couldn't parse end {}", tso_region_view); + auto str_begin = str_end; + end = std::strtoull(str_begin, &str_end, 16); + LOGMAN_THROW_A_FMT(str_begin != str_end, "Couldn't parse end {}", tso_region_view); + + current_module->second.VolatileValidRanges.Insert({begin, end}); + non_tso_region_offset = end_of_region_substr == section_str.npos ? section_str.npos : end_of_region_substr + 1; + } + } + + // Individual instruction handling + if (end_of_address_ranges != module_config.npos) { + end_of_individual_inst = module_config.find(";", end_of_address_ranges + 1); + std::string_view section_str = module_config.substr(end_of_address_ranges + 1, end_of_individual_inst); + + if (section_str.empty()) { + module_offset = end_of_module == ListOfDescriptors.npos ? ListOfDescriptors.npos : end_of_module + 1; + continue; + } + + for (size_t force_tso_region_offset = 0; force_tso_region_offset != section_str.npos;) { + size_t end_of_region_substr = section_str.find(",", force_tso_region_offset); + std::string_view tso_region_view = section_str.substr(force_tso_region_offset, end_of_region_substr - force_tso_region_offset); + + if (tso_region_view.empty()) { + force_tso_region_offset = end_of_region_substr == section_str.npos ? section_str.npos : end_of_region_substr + 1; + continue; + } + + uint64_t offset {}; + char* str_end; + offset = std::strtoull(tso_region_view.data(), &str_end, 16); + LOGMAN_THROW_A_FMT(tso_region_view.data() != str_end, "Couldn't parse offset {}", tso_region_view); + + current_module->second.VolatileInstructions.insert(offset); + + force_tso_region_offset = end_of_region_substr == section_str.npos ? section_str.npos : end_of_region_substr + 1; + } + } + + module_offset = end_of_module == ListOfDescriptors.npos ? ListOfDescriptors.npos : end_of_module + 1; + } + + return ExtendedMetaData; +} +} // namespace FEX::VolatileMetadata diff --git a/Source/Common/VolatileMetadata.h b/Source/Common/VolatileMetadata.h new file mode 100644 index 000000000..21df597c6 --- /dev/null +++ b/Source/Common/VolatileMetadata.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +#pragma once +#include +#include +#include +#include + +namespace FEX::VolatileMetadata { +struct ExtendedVolatileMetadata { + FEXCore::IntervalList VolatileValidRanges; + fextl::set VolatileInstructions; + bool ModuleTSODisabled; +}; + +fextl::unordered_map ParseExtendedVolatileMetadata(std::string_view ListOfDescriptors); +} // namespace FEX::VolatileMetadata diff --git a/Source/Windows/ARM64EC/Module.cpp b/Source/Windows/ARM64EC/Module.cpp index b25fa43c6..aca9e877b 100644 --- a/Source/Windows/ARM64EC/Module.cpp +++ b/Source/Windows/ARM64EC/Module.cpp @@ -43,6 +43,7 @@ $end_info$ #include "DummyHandlers.h" #include "BTInterface.h" #include "Windows/Common/SHMStats.h" +#include "Windows/Common/VolatileMetadata.h" #include #include @@ -80,6 +81,7 @@ void* WineSyscallDispatcher; uint64_t WineNtContinueSyscallId; uint64_t WineNtAllocateVirtualMemorySyscallId; uint64_t WineNtProtectVirtualMemorySyscallId; +static fextl::unordered_map ExtendedMetaData {}; NTSTATUS NtContinueNative(ARM64_NT_CONTEXT* NativeContext, BOOLEAN Alert); NTSTATUS NtAllocateVirtualMemoryNative(HANDLE, PVOID*, ULONG_PTR, SIZE_T*, ULONG, ULONG); @@ -267,11 +269,10 @@ void InitSyscalls() { PatchCallChecker(); } -void LoadImageVolatileMetadata(uint64_t Address) { - const auto Module = reinterpret_cast(Address); - IMAGE_NT_HEADERS* Nt = RtlImageNtHeader(Module); - uint64_t EndAddress = Address + Nt->OptionalHeader.SizeOfImage; +void LoadImageVolatileMetadata(fextl::set& VolatileInstructions, FEXCore::IntervalList& VolatileValidRanges, + HMODULE Module, IMAGE_NT_HEADERS* Nt, uint64_t Address, uint64_t EndAddress) { ULONG Size; + const auto* LoadConfig = reinterpret_cast<_IMAGE_LOAD_CONFIG_DIRECTORY64*>(RtlImageDirectoryEntryToData(Module, true, IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG, &Size)); if (!LoadConfig || LoadConfig->Size <= offsetof(_IMAGE_LOAD_CONFIG_DIRECTORY64, VolatileMetadataPointer)) { @@ -288,7 +289,6 @@ void LoadImageVolatileMetadata(uint64_t Address) { return; } - fextl::set VolatileInstructions; const auto* VolatileAccessTableBegin = reinterpret_cast(Address + VolatileMetadata->VolatileAccessTable); const auto* VolatileAccessTableEnd = VolatileAccessTableBegin + (VolatileMetadata->VolatileAccessTableSize / sizeof(IMAGE_VOLATILE_RVA_METADATA)); @@ -296,13 +296,31 @@ void LoadImageVolatileMetadata(uint64_t Address) { VolatileInstructions.emplace(Address + It->Rva); } - FEXCore::IntervalList VolatileValidRanges; const auto* VolatileInfoRangeTableBegin = reinterpret_cast(Address + VolatileMetadata->VolatileInfoRangeTable); const auto* VolatileInfoRangeTableEnd = VolatileInfoRangeTableBegin + (VolatileMetadata->VolatileInfoRangeTableSize / sizeof(IMAGE_VOLATILE_RANGE_METADATA)); for (auto It = VolatileInfoRangeTableBegin; It != VolatileInfoRangeTableEnd; It++) { VolatileValidRanges.Insert({Address + It->Rva, Address + It->Rva + It->Size}); } +} + +void LoadImageVolatileMetadata(const fextl::string& ModuleName, uint64_t Address) { + const auto Module = reinterpret_cast(Address); + IMAGE_NT_HEADERS* Nt = RtlImageNtHeader(Module); + uint64_t EndAddress = Address + Nt->OptionalHeader.SizeOfImage; + + fextl::set VolatileInstructions {}; + FEXCore::IntervalList VolatileValidRanges {}; + LoadImageVolatileMetadata(VolatileInstructions, VolatileValidRanges, Module, Nt, Address, EndAddress); + + auto it = ExtendedMetaData.find(ModuleName); + if (it != ExtendedMetaData.end()) { + FEX::Windows::ApplyFEXExtendedVolatileMetadata(it->second, VolatileInstructions, VolatileValidRanges, Address, EndAddress); + } + + if (VolatileInstructions.empty() && VolatileValidRanges.Empty()) { + return; + } LogMan::Msg::DFmt("Loaded volatile metadata for {:X}: {} entries", Address, VolatileInstructions.size()); std::scoped_lock Lock(CTX->GetCodeInvalidationMutex()); @@ -314,7 +332,7 @@ void HandleImageMap(uint64_t Address) { LogMan::Msg::DFmt("Load module {}: {:X}", ModuleName, Address); FEX_CONFIG_OPT(VolatileMetadata, VOLATILEMETADATA); if (VolatileMetadata) { - LoadImageVolatileMetadata(Address); + LoadImageVolatileMetadata(ModuleName, Address); } InvalidationTracker->HandleImageMap(ModuleName, Address); } @@ -630,6 +648,9 @@ NTSTATUS ProcessInit() { FEXCore::Context::InitializeStaticTables(FEXCore::Context::MODE_64BIT); + FEX_CONFIG_OPT(ExtendedVolatileMetadataConfig, EXTENDEDVOLATILEMETADATA); + ExtendedMetaData = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(ExtendedVolatileMetadataConfig()); + SignalDelegator = fextl::make_unique(); SyscallHandler = fextl::make_unique(); Exception::HandlerConfig.emplace(); @@ -941,7 +962,7 @@ NTSTATUS ThreadInit() { auto NewSegments = new FEXCore::Core::CPUState::gdt_segment[32]; // Setup initial code-segment GDT - auto &GDT = NewSegments[FEXCore::Core::CPUState::DEFAULT_USER_CS]; + auto& GDT = NewSegments[FEXCore::Core::CPUState::DEFAULT_USER_CS]; FEXCore::Core::CPUState::SetGDTBase(&GDT, 0); FEXCore::Core::CPUState::SetGDTLimit(&GDT, 0xF'FFFFU); GDT.L = 1; // L = Long Mode = 64-bit @@ -1033,7 +1054,7 @@ NTSTATUS ThreadTerm(HANDLE Thread, LONG ExitCode) { auto ThreadState = CPUArea.ThreadState(); // GDT and LDT are mirrored, only free one. - delete [] ThreadState->CurrentFrame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_GDT]; + delete[] ThreadState->CurrentFrame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_GDT]; FEX::Windows::CallRetStack::DestroyThread(ThreadState); CTX->DestroyThread(ThreadState); diff --git a/Source/Windows/Common/VolatileMetadata.h b/Source/Windows/Common/VolatileMetadata.h new file mode 100644 index 000000000..8d6d3f813 --- /dev/null +++ b/Source/Windows/Common/VolatileMetadata.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +#pragma once + +#include "Common/VolatileMetadata.h" + +#include +#include +#include +#include + +namespace FEX::Windows { +inline void ApplyFEXExtendedVolatileMetadata(FEX::VolatileMetadata::ExtendedVolatileMetadata& ExtendedMetaData, + fextl::set& VolatileInstructions, + FEXCore::IntervalList& VolatileValidRanges, uint64_t Address, uint64_t EndAddress) { + // Load FEX extended volatile metadata. + // Walk the volatile instructions first if they exist. + for (const auto it_inst : ExtendedMetaData.VolatileInstructions) { + const auto inst_address = it_inst + Address; + if (inst_address < EndAddress) { + VolatileInstructions.emplace(Address + it_inst); + } else { + LogMan::Msg::DFmt("Volatile instruction 0x{:x} couldn't fit in to module range [0x{:x}, 0x{:x}). Not adding anymore volatile " + "instructions. Inspect your config!", + inst_address, Address, EndAddress); + return; + } + } + + // Walk the volatile list + for (const auto it_ranges : ExtendedMetaData.VolatileValidRanges) { + VolatileValidRanges.Insert({Address + it_ranges.Offset, Address + it_ranges.End}); + } + + // If it is fully disabled, then set the entire module range + if (ExtendedMetaData.ModuleTSODisabled) { + VolatileValidRanges.Clear(); + VolatileValidRanges.Insert({Address, EndAddress}); + } +} + + +} // namespace FEX::Windows diff --git a/Source/Windows/WOW64/Module.cpp b/Source/Windows/WOW64/Module.cpp index 26f2315cd..692553257 100644 --- a/Source/Windows/WOW64/Module.cpp +++ b/Source/Windows/WOW64/Module.cpp @@ -26,6 +26,7 @@ $end_info$ #include #include #include +#include #include "Common/ArgumentLoader.h" #include "Common/CallRetStack.h" @@ -43,6 +44,7 @@ $end_info$ #include "DummyHandlers.h" #include "BTInterface.h" #include "Windows/Common/SHMStats.h" +#include "Windows/Common/VolatileMetadata.h" #include #include @@ -127,6 +129,8 @@ std::mutex ThreadCreationMutex; // Map of TIDs to their FEX thread state, `ThreadCreationMutex` must be locked when accessing std::unordered_map Threads; +static fextl::unordered_map ExtendedMetaData {}; + decltype(__wine_unix_call_dispatcher) WineUnixCall; std::pair GetThreadTLS(HANDLE Thread) { @@ -159,9 +163,36 @@ bool IsAddressInJit(uint64_t Address) { return Thread->CTX->IsAddressInCodeBuffer(Thread, Address); } +void LoadImageVolatileMetadata(const fextl::string& ModuleName, uint64_t Address) { + const auto Module = reinterpret_cast(Address); + IMAGE_NT_HEADERS* Nt = RtlImageNtHeader(Module); + uint64_t EndAddress = Address + Nt->OptionalHeader.SizeOfImage; + + fextl::set VolatileInstructions; + FEXCore::IntervalList VolatileValidRanges; + + // Load FEX extended volatile metadata. + auto it = ExtendedMetaData.find(ModuleName); + if (it != ExtendedMetaData.end()) { + FEX::Windows::ApplyFEXExtendedVolatileMetadata(it->second, VolatileInstructions, VolatileValidRanges, Address, EndAddress); + } + + if (VolatileInstructions.empty() && VolatileValidRanges.Empty()) { + return; + } + + LogMan::Msg::DFmt("Loaded volatile metadata for {:X}: {} entries", Address, VolatileInstructions.size()); + std::scoped_lock Lock(CTX->GetCodeInvalidationMutex()); + CTX->AddForceTSOInformation(VolatileValidRanges, std::move(VolatileInstructions)); +} + void HandleImageMap(uint64_t Address) { fextl::string ModuleName = FEX::Windows::GetSectionFilePath(Address); LogMan::Msg::DFmt("Load module {}: {:X}", ModuleName, Address); + FEX_CONFIG_OPT(VolatileMetadata, VOLATILEMETADATA); + if (VolatileMetadata) { + LoadImageVolatileMetadata(ModuleName, Address); + } InvalidationTracker->HandleImageMap(ModuleName, Address); } } // namespace @@ -494,6 +525,9 @@ void BTCpuProcessInit() { FEXCore::Context::InitializeStaticTables(FEXCore::Context::MODE_32BIT); + FEX_CONFIG_OPT(ExtendedVolatileMetadataConfig, EXTENDEDVOLATILEMETADATA); + ExtendedMetaData = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(ExtendedVolatileMetadataConfig()); + SignalDelegator = fextl::make_unique(); SyscallHandler = fextl::make_unique(); Context::HandlerConfig.emplace(); @@ -572,7 +606,7 @@ void BTCpuThreadInit() { auto NewSegments = new FEXCore::Core::CPUState::gdt_segment[32]; // Setup initial code-segment GDT - auto &GDT = NewSegments[FEXCore::Core::CPUState::DEFAULT_USER_CS]; + auto& GDT = NewSegments[FEXCore::Core::CPUState::DEFAULT_USER_CS]; FEXCore::Core::CPUState::SetGDTBase(&GDT, 0); FEXCore::Core::CPUState::SetGDTLimit(&GDT, 0xF'FFFFU); GDT.L = 0; // L = Long Mode = 32-bit @@ -638,7 +672,7 @@ void BTCpuThreadTerm(HANDLE Thread, LONG ExitCode) { auto ThreadState = TLS.ThreadState(); // GDT and LDT are mirrored, only free one. - delete [] ThreadState->CurrentFrame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_GDT]; + delete[] ThreadState->CurrentFrame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_GDT]; FEX::Windows::CallRetStack::DestroyThread(ThreadState); CTX->DestroyThread(ThreadState); @@ -734,7 +768,7 @@ __attribute__((naked)) void BTCpuSimulate() { ".seh_endproc;"); } -extern "C" void BTCpuSimulateImpl(CONTEXT *entry_context) { +extern "C" void BTCpuSimulateImpl(CONTEXT* entry_context) { auto TLS = GetTLS(); TLS.EntryContext() = entry_context; TLS.CachedCallRetSp() = TLS.ThreadState()->CurrentFrame->State.callret_sp; From 0dfc30fdd8f2c5a3db92e2b12ac2366fbba9a9fd Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Tue, 19 Aug 2025 12:21:46 -0700 Subject: [PATCH 3/3] APITests: Adds extendedvolatile --- unittests/APITests/CMakeLists.txt | 1 + .../APITests/ExtendedVolatileMetadata.cpp | 158 ++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 unittests/APITests/ExtendedVolatileMetadata.cpp diff --git a/unittests/APITests/CMakeLists.txt b/unittests/APITests/CMakeLists.txt index 071bb0a3f..260f1be47 100644 --- a/unittests/APITests/CMakeLists.txt +++ b/unittests/APITests/CMakeLists.txt @@ -5,6 +5,7 @@ set (TESTS Filesystem StringUtils fextl_function + ExtendedVolatileMetadata ) list(APPEND LIBS Common FEXCore JemallocLibs) diff --git a/unittests/APITests/ExtendedVolatileMetadata.cpp b/unittests/APITests/ExtendedVolatileMetadata.cpp new file mode 100644 index 000000000..4c0654828 --- /dev/null +++ b/unittests/APITests/ExtendedVolatileMetadata.cpp @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +#include +#include "Common/VolatileMetadata.h" + +TEST_CASE("Basic - Empty") { + const auto String = ""; + const auto Result = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(String); + REQUIRE(Result.empty()); +} + +TEST_CASE("Basic - Empty - modules") { + const auto String = ":::::"; + const auto Result = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(String); + REQUIRE(Result.empty()); +} + +TEST_CASE("Basic - Single") { + const auto String = "hl2_linux"; + const auto Result = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(String); + REQUIRE(Result.size() == 1); + + REQUIRE(Result.contains("hl2_linux")); + CHECK(Result.at("hl2_linux").ModuleTSODisabled == true); + CHECK(Result.at("hl2_linux").VolatileInstructions.empty()); + CHECK(Result.at("hl2_linux").VolatileValidRanges.Empty()); +} + +TEST_CASE("Basic - Multiple") { + const auto String = "hl2_linux:DeckJob"; + const auto Result = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(String); + REQUIRE(Result.size() == 2); + + REQUIRE(Result.contains("hl2_linux")); + CHECK(Result.at("hl2_linux").ModuleTSODisabled == true); + CHECK(Result.at("hl2_linux").VolatileInstructions.empty()); + CHECK(Result.at("hl2_linux").VolatileValidRanges.Empty()); + + REQUIRE(Result.contains("DeckJob")); + CHECK(Result.at("DeckJob").ModuleTSODisabled == true); + CHECK(Result.at("DeckJob").VolatileInstructions.empty()); + CHECK(Result.at("DeckJob").VolatileValidRanges.Empty()); +} + +TEST_CASE("Basic - Single plus empty") { + const auto String = "hl2_linux:::::"; + const auto Result = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(String); + REQUIRE(Result.size() == 1); + REQUIRE(Result.contains("hl2_linux")); + CHECK(Result.at("hl2_linux").ModuleTSODisabled == true); + CHECK(Result.at("hl2_linux").VolatileInstructions.empty()); + CHECK(Result.at("hl2_linux").VolatileValidRanges.Empty()); +} + +static inline bool ContainsRange(std::pair Range, const std::vector>& ValidRanges) { + return std::ranges::find(ValidRanges, Range) != ValidRanges.end(); +} + +TEST_CASE("Basic - Single - offset") { + const auto String = "hl2_linux;0x0-0x1000"; + const auto Result = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(String); + REQUIRE(Result.size() == 1); + + REQUIRE(Result.contains("hl2_linux")); + CHECK(Result.at("hl2_linux").ModuleTSODisabled == false); + CHECK(Result.at("hl2_linux").VolatileInstructions.empty()); + CHECK(Result.at("hl2_linux").VolatileValidRanges.Empty() == false); + + const std::vector> ValidRanges = { + {0, 0x1000}, + }; + + for (auto it : Result.at("hl2_linux").VolatileValidRanges) { + CHECK(ContainsRange(std::make_pair(it.Offset, it.End), ValidRanges)); + } +} + +TEST_CASE("Basic - Single - offset x2") { + const auto String = "hl2_linux;0x0-0x1000,0x2000-0x3000"; + const auto Result = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(String); + REQUIRE(Result.size() == 1); + REQUIRE(Result.contains("hl2_linux")); + CHECK(Result.at("hl2_linux").ModuleTSODisabled == false); + CHECK(Result.at("hl2_linux").VolatileInstructions.empty()); + CHECK(Result.at("hl2_linux").VolatileValidRanges.Empty() == false); + + const std::vector> ValidRanges = { + {0, 0x1000}, + {0x2000, 0x3000}, + }; + + for (auto it : Result.at("hl2_linux").VolatileValidRanges) { + CHECK(ContainsRange(std::make_pair(it.Offset, it.End), ValidRanges)); + } +} + +TEST_CASE("Basic - Single - offset plus instruction") { + const auto String = "hl2_linux;0x0-0x1000;0x1,0x2,0x3"; + const auto Result = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(String); + REQUIRE(Result.size() == 1); + REQUIRE(Result.contains("hl2_linux")); + CHECK(Result.at("hl2_linux").ModuleTSODisabled == false); + CHECK(Result.at("hl2_linux").VolatileInstructions.empty() == false); + CHECK(Result.at("hl2_linux").VolatileValidRanges.Empty() == false); + + const std::vector> ValidRanges = { + {0, 0x1000}, + }; + + const std::vector ValidInsts = { + 1, + 2, + 3, + }; + + for (auto it : Result.at("hl2_linux").VolatileValidRanges) { + CHECK(ContainsRange(std::make_pair(it.Offset, it.End), ValidRanges)); + } + + for (auto it : Result.at("hl2_linux").VolatileInstructions) { + CHECK_THAT(ValidInsts, Catch::Matchers::Contains(it)); + } +} + +TEST_CASE("Basic - Double - offset") { + const auto String = "hl2_linux;0x0-0x1000:DeckJob;0x2000-0x3000"; + const auto Result = FEX::VolatileMetadata::ParseExtendedVolatileMetadata(String); + REQUIRE(Result.size() == 2); + + { + REQUIRE(Result.contains("hl2_linux")); + CHECK(Result.at("hl2_linux").ModuleTSODisabled == false); + CHECK(Result.at("hl2_linux").VolatileInstructions.empty()); + CHECK(Result.at("hl2_linux").VolatileValidRanges.Empty() == false); + + const std::vector> ValidRanges = { + {0, 0x1000}, + }; + + for (auto it : Result.at("hl2_linux").VolatileValidRanges) { + CHECK(ContainsRange(std::make_pair(it.Offset, it.End), ValidRanges)); + } + } + + { + REQUIRE(Result.contains("DeckJob")); + CHECK(Result.at("DeckJob").ModuleTSODisabled == false); + CHECK(Result.at("DeckJob").VolatileInstructions.empty()); + CHECK(Result.at("DeckJob").VolatileValidRanges.Empty() == false); + + const std::vector> ValidRanges = { + {0x2000, 0x3000}, + }; + + for (auto it : Result.at("DeckJob").VolatileValidRanges) { + CHECK(ContainsRange(std::make_pair(it.Offset, it.End), ValidRanges)); + } + } +}