diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 243ccba3c4..d2c3615517 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -7,7 +7,6 @@ endif(NOT TOP_CMAKE_WAS_SOURCED) add_library(common) -# x86emitter sources target_sources(common PRIVATE AlignedMalloc.cpp Assertions.cpp @@ -34,9 +33,9 @@ target_sources(common PRIVATE Timer.cpp WAVWriter.cpp WindowInfo.cpp + YAML.cpp ) -# x86emitter headers target_sources(common PRIVATE AlignedMalloc.h Assertions.h @@ -81,6 +80,7 @@ target_sources(common PRIVATE WAVWriter.h WindowInfo.h WrappedMemCopy.h + YAML.h ) if(_M_X86) @@ -208,6 +208,7 @@ target_link_libraries(common PRIVATE target_link_libraries(common PUBLIC fmt::fmt fast_float + rapidyaml::rapidyaml ) fixup_file_properties(common) diff --git a/common/YAML.cpp b/common/YAML.cpp new file mode 100644 index 0000000000..af6ca6bbaf --- /dev/null +++ b/common/YAML.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "YAML.h" + +#include +#include + +struct RapidYAMLContext +{ + std::jmp_buf env; + Error* error = nullptr; +}; + +std::optional ParseYAMLFromString(ryml::csubstr yaml, ryml::csubstr file_name, Error* error) +{ + RapidYAMLContext context; + context.error = error; + + ryml::Callbacks callbacks; + callbacks.m_user_data = static_cast(&context); + callbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location location, void* user_data) { + RapidYAMLContext* context = static_cast(user_data); + + Error::SetString(context->error, std::string(msg, msg_len)); + std::longjmp(context->env, 1); + }; + + ryml::EventHandlerTree event_handler(callbacks); + ryml::Parser parser(&event_handler); + + ryml::Tree tree; + + // The only options RapidYAML provides for recovering from errors are + // throwing an exception or using setjmp/longjmp. Since we have exceptions + // disabled we have to use the latter option. + if (setjmp(context.env)) + return std::nullopt; + + ryml::parse_in_arena(&parser, file_name, yaml, &tree); + + return tree; +} diff --git a/common/YAML.h b/common/YAML.h new file mode 100644 index 0000000000..01d9d59e97 --- /dev/null +++ b/common/YAML.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "Error.h" + +#include "ryml_std.hpp" +#include "ryml.hpp" +#include "ryml.hpp" + +#include + +/// Parse a YAML file with RapidYAML, and use setjmp/longjmp to recover from +/// parsing errors (as is recommended by the documentation for cases where +/// exceptions are disabled). The file_name parameter is only used for error +/// messages, which are returned via the error parameter. +std::optional ParseYAMLFromString(ryml::csubstr yaml, ryml::csubstr file_name, Error* error); diff --git a/common/common.vcxproj b/common/common.vcxproj index d8efcfdaa6..eb3d734578 100644 --- a/common/common.vcxproj +++ b/common/common.vcxproj @@ -36,10 +36,12 @@ %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\fast_float\include %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\fmt\include %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\jpgd + %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rapidyaml\include Use PrecompiledHeader.h PrecompiledHeader.h $(IntDir)%(RelativeDir) + C4_NO_DEBUG_BREAK;%(PreprocessorDefinitions) @@ -71,6 +73,7 @@ + Create @@ -157,6 +160,7 @@ + diff --git a/common/common.vcxproj.filters b/common/common.vcxproj.filters index 4b04b516c0..c9715b3556 100644 --- a/common/common.vcxproj.filters +++ b/common/common.vcxproj.filters @@ -127,6 +127,9 @@ Source Files + + Source Files + @@ -335,6 +338,9 @@ + + Header Files + @@ -349,4 +355,4 @@ Source Files - \ No newline at end of file + diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index c66a27eeec..cacbebacb3 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -1140,7 +1140,6 @@ target_link_libraries(PCSX2_FLAGS INTERFACE common imgui fmt::fmt - rapidyaml::rapidyaml libchdr libzip::zip cpuinfo diff --git a/pcsx2/GameDatabase.cpp b/pcsx2/GameDatabase.cpp index bdb28e3c9d..b90c4ad5ce 100644 --- a/pcsx2/GameDatabase.cpp +++ b/pcsx2/GameDatabase.cpp @@ -14,6 +14,7 @@ #include "common/Path.h" #include "common/StringUtil.h" #include "common/Timer.h" +#include "common/YAML.h" #include #include "ryml_std.hpp" @@ -357,8 +358,7 @@ static const char* s_round_modes[static_cast(FPRoundMode::MaxCount)] = { "Nearest", "NegativeInfinity", "PositiveInfinity", - "Chop" -}; + "Chop"}; static const char* s_gs_hw_fix_names[] = { "autoFlush", @@ -930,7 +930,7 @@ void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions& Host::AddKeyedOSDMessage("HWFixesWarning", fmt::format(ICON_FA_WAND_MAGIC_SPARKLES " {}\n{}", TRANSLATE_SV("GameDatabase", "Manual GS hardware renderer fixes are enabled, automatic fixes were not applied:"), - disabled_fixes), + disabled_fixes), Host::OSD_ERROR_DURATION); } else @@ -941,25 +941,28 @@ void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions& void GameDatabase::initDatabase() { - ryml::Callbacks rymlCallbacks = ryml::get_callbacks(); - rymlCallbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location loc, void* userdata) { - Console.Error(fmt::format("[GameDB YAML] Parsing error at {}:{} (bufpos={}): {}", - loc.line, loc.col, loc.offset, std::string_view(msg, msg_len))); - }; - ryml::set_callbacks(rymlCallbacks); - ryml::set_error_callback([](const char* msg, size_t msg_size) { - Console.Error(fmt::format("[GameDB YAML] Internal Parsing error: {}", std::string_view(msg, msg_size))); - }); + const std::string path(Path::Combine(EmuFolders::Resources, GAMEDB_YAML_FILE_NAME)); + const std::string name(GAMEDB_YAML_FILE_NAME); - auto buf = FileSystem::ReadFileToString(Path::Combine(EmuFolders::Resources, GAMEDB_YAML_FILE_NAME).c_str()); - if (!buf.has_value()) + const std::optional buffer = FileSystem::ReadFileToString(path.c_str()); + if (!buffer.has_value()) { Console.Error("GameDB: Unable to open GameDB file, file does not exist."); return; } - ryml::Tree tree = ryml::parse_in_arena(ryml::to_csubstr(buf.value())); - ryml::NodeRef root = tree.rootref(); + const ryml::csubstr yaml = ryml::to_csubstr(*buffer); + + Error error; + std::optional tree = ParseYAMLFromString(yaml, ryml::to_csubstr(name), &error); + if (!tree.has_value()) + { + Console.ErrorFmt("GameDB: Failed to parse game database file {}:", path); + Console.Error(error.GetDescription()); + return; + } + + ryml::NodeRef root = tree->rootref(); for (const ryml::NodeRef& n : root.children()) { @@ -971,7 +974,7 @@ void GameDatabase::initDatabase() // However, YAML's keys are as expected case-sensitive, so we have to explicitly do our own duplicate checking if (s_game_db.count(serial) == 1) { - Console.Error(fmt::format("GameDB: Duplicate serial '{}' found in GameDB. Skipping, Serials are case-insensitive!", serial)); + Console.ErrorFmt("GameDB: Duplicate serial '{}' found in GameDB. Skipping, Serials are case-insensitive!", serial); continue; } @@ -980,8 +983,6 @@ void GameDatabase::initDatabase() parseAndInsert(serial, n); } } - - ryml::reset_callbacks(); } void GameDatabase::ensureLoaded() @@ -1069,7 +1070,7 @@ static bool parseHashDatabaseEntry(const ryml::NodeRef& node) { if (!n.is_map() || !n.has_child("size") || !n.has_child("md5")) { - Console.Error(fmt::format("[HashDatabase] Incomplete hash definition in {}", entry.name)); + Console.ErrorFmt("[HashDatabase] Incomplete hash definition in {}", entry.name); return false; } @@ -1080,12 +1081,12 @@ static bool parseHashDatabaseEntry(const ryml::NodeRef& node) if (!th.parseHash(md5)) { - Console.Error(fmt::format("[HashDatabase] Failed to parse hash in {}: '{}'", entry.name, md5)); + Console.ErrorFmt("[HashDatabase] Failed to parse hash in {}: '{}'", entry.name, md5); return false; } if (entry.tracks.empty() && s_track_hash_to_entry_map.find(th) != s_track_hash_to_entry_map.end()) - Console.Warning(fmt::format("[HashDatabase] Duplicate first track hash in {}", entry.name)); + Console.WarningFmt("[HashDatabase] Duplicate first track hash in {}", entry.name); entry.tracks.push_back(th); s_track_hash_to_entry_map.emplace(th, index); @@ -1100,27 +1101,30 @@ bool GameDatabase::loadHashDatabase() if (!s_hash_database.empty()) return true; - ryml::Callbacks rymlCallbacks = ryml::get_callbacks(); - rymlCallbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location loc, void*) { - Console.Error(fmt::format( - "[HashDatabase YAML] Parsing error at {}:{} (bufpos={}): {}", loc.line, loc.col, loc.offset, msg)); - }; - ryml::set_callbacks(rymlCallbacks); - ryml::set_error_callback([](const char* msg, size_t msg_size) { - Console.Error(fmt::format("[HashDatabase YAML] Internal Parsing error: {}", std::string_view(msg, msg_size))); - }); - Common::Timer load_timer; - auto buf = FileSystem::ReadFileToString(Path::Combine(EmuFolders::Resources, HASHDB_YAML_FILE_NAME).c_str()); - if (!buf.has_value()) + const std::string path(Path::Combine(EmuFolders::Resources, HASHDB_YAML_FILE_NAME)); + const std::string name(HASHDB_YAML_FILE_NAME); + + std::optional buffer = FileSystem::ReadFileToString(path.c_str()); + if (!buffer.has_value()) { - Console.Error("GameDB: Unable to open hash database file, file does not exist."); + Console.Error("[HashDatabase] Unable to open hash database file, file does not exist."); return false; } - ryml::Tree tree = ryml::parse_in_arena(ryml::to_csubstr(buf.value())); - ryml::NodeRef root = tree.rootref(); + ryml::csubstr yaml = ryml::to_csubstr(*buffer); + + Error error; + std::optional tree = ParseYAMLFromString(yaml, ryml::to_csubstr(name), &error); + if (!tree.has_value()) + { + Console.ErrorFmt("[HashDatabase] Failed to parse hash database file {}:", path); + Console.Error(error.GetDescription()); + return false; + } + + ryml::NodeRef root = tree->rootref(); bool okay = true; for (const ryml::NodeRef& n : root.children()) @@ -1132,7 +1136,6 @@ bool GameDatabase::loadHashDatabase() } } - ryml::reset_callbacks(); if (!okay) { s_track_hash_to_entry_map.clear();