#include "Cafe/GraphicPack/GraphicPack2.h" #include "config/CemuConfig.h" #include "config/ActiveSettings.h" #include "openssl/sha.h" #include "Cafe/HW/Latte/Renderer/RendererOuputShader.h" #include "Cafe/Filesystem/fsc.h" #include "boost/algorithm/string.hpp" #include "util/helpers/MapAdaptor.h" #include "util/helpers/StringParser.h" #include "Cafe/HW/Latte/Core/LatteTiming.h" #include "util/IniParser/IniParser.h" #include "util/helpers/StringHelpers.h" #include "Cafe/CafeSystem.h" std::vector GraphicPack2::s_graphic_packs; std::vector GraphicPack2::s_active_graphic_packs; std::atomic_bool GraphicPack2::s_isReady; #define GP_LEGACY_VERSION (2) void GraphicPack2::LoadGraphicPack(fs::path graphicPackPath) { fs::path rulesPath = graphicPackPath; rulesPath.append("rules.txt"); std::unique_ptr fs_rules(FileStream::openFile2(rulesPath)); if (!fs_rules) return; std::vector rulesData; fs_rules->extract(rulesData); IniParser iniParser(rulesData, rulesPath.string()); if (!iniParser.NextSection()) { cemuLog_force("{}: Does not contain any sections", _utf8Wrapper(rulesPath)); return; } if (!boost::iequals(iniParser.GetCurrentSectionName(), "Definition")) { cemuLog_force("{}: [Definition] must be the first section", _utf8Wrapper(rulesPath)); return; } auto option_version = iniParser.FindOption("version"); if (option_version) { sint32 versionNum = -1; auto [ptr, ec] = std::from_chars(option_version->data(), option_version->data() + option_version->size(), versionNum); if (ec != std::errc{}) { cemuLog_force("{}: Unable to parse version", _utf8Wrapper(rulesPath)); return; } if (versionNum > GP_LEGACY_VERSION) { GraphicPack2::LoadGraphicPack(rulesPath.generic_wstring(), iniParser); return; } } cemuLog_force("{}: Outdated graphic pack", _utf8Wrapper(rulesPath)); } void GraphicPack2::LoadAll() { std::error_code ec; fs::path basePath = ActiveSettings::GetPath("graphicPacks"); for (fs::recursive_directory_iterator it(basePath, ec); it != end(it); ++it) { if (!it->is_directory(ec)) continue; fs::path gfxPackPath = it->path(); if (fs::exists(gfxPackPath / "rules.txt", ec)) { LoadGraphicPack(gfxPackPath); it.disable_recursion_pending(); // dont recurse deeper in a gfx pack directory continue; } } } bool GraphicPack2::LoadGraphicPack(const std::wstring& filename, IniParser& rules) { try { auto gp = std::make_shared(filename, rules); // check if enabled and preset set const auto& config_entries = g_config.data().graphic_pack_entries; // legacy absolute path checking for not breaking compatibility auto file = gp->GetFilename2(); auto it = config_entries.find(file.lexically_normal()); if (it == config_entries.cend()) { // check for relative path it = config_entries.find(MakeRelativePath(gp->GetFilename2()).lexically_normal()); } if (it != config_entries.cend()) { bool enabled = true; for (auto& kv : it->second) { if (boost::iequals(kv.first, "_disabled")) { enabled = false; continue; } gp->SetActivePreset(kv.first, kv.second, false); } gp->SetEnabled(enabled); } gp->UpdatePresetVisibility(); gp->ValidatePresetSelections(); s_graphic_packs.emplace_back(gp); return true; } catch (const std::exception&) { return false; } } bool GraphicPack2::ActivateGraphicPack(const std::shared_ptr& graphic_pack) { if (graphic_pack->Activate()) { s_active_graphic_packs.push_back(graphic_pack); return true; } return false; } bool GraphicPack2::DeactivateGraphicPack(const std::shared_ptr& graphic_pack) { if (!graphic_pack->IsActivated()) return false; const auto it = std::find_if(s_active_graphic_packs.begin(), s_active_graphic_packs.end(), [graphic_pack](const GraphicPackPtr& gp) { return gp->GetFilename() == graphic_pack->GetFilename(); } ); if (it == s_active_graphic_packs.end()) return false; graphic_pack->Deactivate(); s_active_graphic_packs.erase(it); return true; } void GraphicPack2::ActivateForCurrentTitle() { uint64 titleId = CafeSystem::GetForegroundTitleId(); // activate graphic packs for (const auto& gp : GraphicPack2::GetGraphicPacks()) { if (!gp->IsEnabled()) continue; if (!gp->ContainsTitleId(titleId)) continue; if (GraphicPack2::ActivateGraphicPack(gp)) { if (gp->GetPresets().empty()) { forceLog_printf("Activate graphic pack: %s", gp->GetPath().c_str()); } else { std::string logLine; logLine.assign(fmt::format("Activate graphic pack: {} [Presets: ", gp->GetPath())); bool isFirst = true; for (auto& itr : gp->GetPresets()) { if (!itr->active) continue; if (isFirst) isFirst = false; else logLine.append(","); logLine.append(itr->name); } logLine.append("]"); cemuLog_log(LogType::Force, logLine); } } } s_isReady = true; } void GraphicPack2::Reset() { s_active_graphic_packs.clear(); s_isReady = false; } void GraphicPack2::ClearGraphicPacks() { s_graphic_packs.clear(); s_active_graphic_packs.clear(); } void GraphicPack2::WaitUntilReady() { while (!s_isReady) std::this_thread::sleep_for(std::chrono::milliseconds(5)); } GraphicPack2::GraphicPack2(std::wstring filename) : m_filename(std::move(filename)) { // unused for now } std::unordered_map GraphicPack2::ParsePresetVars(IniParser& rules) const { ExpressionParser parser; std::unordered_map vars; for(auto& itr : rules.GetAllOptions()) { auto option_name = itr.first; auto option_value = itr.second; if (option_name.empty() || option_name[0] != '$') continue; VarType type = kDouble; std::string name(option_name); const auto index = name.find(':'); if(index != std::string::npos) { auto type_name = name.substr(index + 1); name = name.substr(0, index); trim(name); trim(type_name); if (type_name == "double") type = kDouble; else if (type_name == "int") type = kInt; } const double value = parser.Evaluate(option_value); vars.try_emplace(name, std::make_pair(type, value)); parser.AddConstant(name, value); } return vars; } GraphicPack2::GraphicPack2(std::wstring filename, IniParser& rules) : m_filename(std::move(filename)) { // we're already in [Definition] auto option_version = rules.FindOption("version"); if (!option_version) throw std::exception(); m_version = StringHelpers::ToInt(*option_version, -1); if (m_version < 0) { cemuLog_force(L"{}: Invalid version", m_filename); throw std::exception(); } auto option_rendererFilter = rules.FindOption("rendererFilter"); if (option_rendererFilter) { if (boost::iequals(*option_rendererFilter, "vulkan")) m_renderer_api = RendererAPI::Vulkan; else if (boost::iequals(*option_rendererFilter, "opengl")) m_renderer_api = RendererAPI::OpenGL; else cemuLog_force("Unknown value '{}' for rendererFilter option", *option_rendererFilter); } auto option_defaultEnabled = rules.FindOption("default"); if(option_defaultEnabled) { m_default_enabled = boost::iequals(*option_defaultEnabled, "true") || boost::iequals(*option_defaultEnabled, "1"); m_enabled = m_default_enabled; } auto option_vendorFilter = rules.FindOption("vendorFilter"); if (option_vendorFilter) { if (boost::iequals(*option_vendorFilter, "amd")) m_gfx_vendor = GfxVendor::AMD; else if (boost::iequals(*option_vendorFilter, "intel")) m_gfx_vendor = GfxVendor::Intel; else if (boost::iequals(*option_vendorFilter, "mesa")) m_gfx_vendor = GfxVendor::Mesa; else if (boost::iequals(*option_vendorFilter, "nvidia")) m_gfx_vendor = GfxVendor::Nvidia; else if (boost::iequals(*option_vendorFilter, "apple")) m_gfx_vendor = GfxVendor::Apple; else cemuLog_force("Unknown value '{}' for vendorFilter", *option_vendorFilter); } auto option_path = rules.FindOption("path"); if (!option_path) { auto gp_name_log = rules.FindOption("name"); cemuLog_force("[Definition] section from '{}' graphic pack must contain option: path", gp_name_log.has_value() ? *gp_name_log : "Unknown"); throw std::exception(); } m_path = *option_path; auto option_gp_name = rules.FindOption("name"); if (option_gp_name) m_name = *option_gp_name; auto option_description = rules.FindOption("description"); if (option_description) { m_description = *option_description; std::replace(m_description.begin(), m_description.end(), '|', '\n'); } m_title_ids = ParseTitleIds(rules, "titleIds"); if(m_title_ids.empty()) throw std::exception(); auto option_fsPriority = rules.FindOption("fsPriority"); if (option_fsPriority) { std::string tmp(*option_fsPriority); m_fs_priority = std::stoi(tmp); } // load presets while (rules.NextSection()) { auto currentSectionName = rules.GetCurrentSectionName(); if (boost::iequals(currentSectionName, "Default")) { m_preset_vars = ParsePresetVars(rules); } else if (boost::iequals(currentSectionName, "Preset")) { const auto preset_name = rules.FindOption("name"); if (!preset_name) { cemuLog_force("Graphic pack \"{}\": Preset in line {} skipped because it has no name option defined", m_name, rules.GetCurrentSectionLineNumber()); continue; } const auto category = rules.FindOption("category"); const auto condition = rules.FindOption("condition"); const auto default_selected = rules.FindOption("default"); try { const auto vars = ParsePresetVars(rules); PresetPtr preset; if (category && condition) preset = std::make_shared(*category, *preset_name, *condition, vars); else if (category) preset = std::make_shared(*category, *preset_name, vars); else preset = std::make_shared(*preset_name, vars); if (default_selected) preset->is_default = StringHelpers::ToInt(*default_selected) != 0; m_presets.emplace_back(preset); } catch (const std::exception & ex) { cemuLog_force("Graphic pack \"{}\": Can't parse preset \"{}\": {}", m_name, *preset_name, ex.what()); } } else if (boost::iequals(currentSectionName, "RAM")) { for (uint32 i = 0; i < 32; i++) { char optionNameBuf[64]; *fmt::format_to(optionNameBuf, "mapping{}", i) = '\0'; const auto mappingOption = rules.FindOption(optionNameBuf); if (mappingOption) { if (m_version <= 5) { cemuLog_force("Graphic pack \"{}\": [RAM] options are only available for graphic pack version 6 or higher", m_name, optionNameBuf); throw std::exception(); } StringTokenParser parser(*mappingOption); uint32 addrStart = 0, addrEnd = 0; if (parser.parseU32(addrStart) && parser.matchWordI("-") && parser.parseU32(addrEnd) && parser.isEndOfString()) { if (addrEnd <= addrStart) { cemuLog_force("Graphic pack \"{}\": start address (0x{:08x}) must be greater than end address (0x{:08x}) for {}", m_name, addrStart, addrEnd, optionNameBuf); throw std::exception(); } else if ((addrStart & 0xFFF) != 0 || (addrEnd & 0xFFF) != 0) { cemuLog_force("Graphic pack \"{}\": addresses for %s are not aligned to 0x1000", m_name, optionNameBuf); throw std::exception(); } else { m_ramMappings.emplace_back(addrStart, addrEnd); } } else { cemuLog_force("Graphic pack \"{}\": has invalid syntax for option {}", m_name, optionNameBuf); throw std::exception(); } } } } } if (m_version >= 5) { // store by category std::unordered_map> tmp_map; // all vars must be defined in the default preset vars before for (const auto& entry : m_presets) { tmp_map[entry->category].emplace_back(entry); for (auto& kv : entry->variables) { const auto it = m_preset_vars.find(kv.first); if (it == m_preset_vars.cend()) { cemuLog_force("Graphic pack: \"{}\" contains preset variables which are not defined in the default section", m_name); throw std::exception(); } // overwrite var type with default var type kv.second.first = it->second.first; } } // have first entry be default active for every category if no default= is set for(auto entry : get_values(tmp_map)) { if (!entry.empty()) { const auto it = std::find_if(entry.cbegin(), entry.cend(), [](const PresetPtr& preset) { return preset->is_default; }); if (it != entry.cend()) (*it)->active = true; else (*entry.begin())->active = true; } } } else { // verify preset data to contain the same keys std::unordered_map> tmp_map; for (const auto& entry : m_presets) tmp_map[entry->category].emplace_back(entry); for (const auto& kv : tmp_map) { bool has_default = false; for (size_t i = 0; i + 1 < kv.second.size(); ++i) { auto& p1 = kv.second[i]; auto& p2 = kv.second[i + 1]; if (p1->variables.size() != p2->variables.size()) { cemuLog_force("Graphic pack: \"{}\" contains inconsistent preset variables", m_name); throw std::exception(); } std::set keys1(get_keys(p1->variables).begin(), get_keys(p1->variables).end()); std::set keys2(get_keys(p2->variables).begin(), get_keys(p2->variables).end()); if (keys1 != keys2) { cemuLog_force("Graphic pack: \"{}\" contains inconsistent preset variables", m_name); throw std::exception(); } if(p1->is_default) { if(has_default) cemuLog_force("Graphic pack: \"{}\" has more than one preset with the default key set for the same category \"{}\"", m_name, p1->name); p1->active = true; has_default = true; } } // have first entry by default active if no default is set if (!has_default) kv.second[0]->active = true; } } } bool GraphicPack2::Reload() { Deactivate(); return Activate(); } bool GraphicPack2::ContainsTitleId(uint64_t title_id) const { const auto it = std::find_if(m_title_ids.begin(), m_title_ids.end(), [title_id](uint64 id) { return id == title_id; }); return it != m_title_ids.end(); } bool GraphicPack2::HasActivePreset() const { return std::any_of(m_presets.cbegin(), m_presets.cend(), [](const PresetPtr& preset) { return preset->active; }); } std::string GraphicPack2::GetActivePreset(std::string_view category) const { const auto it = std::find_if(m_presets.cbegin(), m_presets.cend(), [category](const PresetPtr& preset) { return preset->active && preset->category == category; }); return it != m_presets.cend() ? (*it)->name : std::string{ "" }; } void GraphicPack2::UpdatePresetVisibility() { // update visiblity of each preset std::for_each(m_presets.begin(), m_presets.end(), [this](PresetPtr& p) { p->visible = m_version >= 5 ? IsPresetVisible(p) : true; }); } void GraphicPack2::ValidatePresetSelections() { if (m_version < 5) return; // only applies to new categorized presets // if any preset is changed then other categories might be affected indirectly // // example: selecting the aspect ratio in a resolution graphic pack would change the available presets in the resolution category // how to handle: select the first available resolution (or the one marked as default) // // example: a preset category might be hidden entirely (e.g. due to a separate advanced options dropdown) // how to handle: leave the previously selected preset // // the logic is therefore as follows: // if there is a preset category with at least 1 visible preset entry then make sure one of those is actually selected // for completely hidden preset categories we leave the selection as-is std::vector order; std::unordered_map> categorizedPresets = GraphicPack2::GetCategorizedPresets(order); bool changedPresets = false; for (auto& categoryItr : categorizedPresets) { // get selection of this category size_t numVisiblePresets = 0; GraphicPack2::PresetPtr defaultSelection = nullptr; GraphicPack2::PresetPtr selectedPreset = nullptr; for (auto& presetItr : categoryItr.second) { if (presetItr->visible) { numVisiblePresets++; if (!defaultSelection || presetItr->is_default) // the preset marked as default becomes the default selection, otherwise pick first visible one defaultSelection = presetItr; } if (presetItr->active) { if (selectedPreset) { // multiple selections inside the same group are invalid presetItr->active = false; changedPresets = true; } else selectedPreset = presetItr; } } if (numVisiblePresets == 0) continue; // do not touch selection if (!selectedPreset) { // no selection at all if (defaultSelection) { selectedPreset = defaultSelection; selectedPreset->active = true; } continue; } // if the currently selected preset is invisible, update it to the preferred visible selection if (!selectedPreset->visible) { selectedPreset->active = false; defaultSelection->active = true; changedPresets = true; } } if (changedPresets) UpdatePresetVisibility(); } bool GraphicPack2::SetActivePreset(std::string_view category, std::string_view name, bool update_visibility) { // disable currently active preset std::for_each(m_presets.begin(), m_presets.end(), [category](PresetPtr& p) { if(p->category == category) p->active = false; }); if (name.empty()) return true; // enable new preset const auto it = std::find_if(m_presets.cbegin(), m_presets.cend(), [category, name](const PresetPtr& preset) { return preset->category == category && preset->name == name; }); bool result; if (it != m_presets.cend()) { (*it)->active = true; cemu_assert_debug(std::count_if(m_presets.cbegin(), m_presets.cend(), [category](const PresetPtr& p) { return p->category == category && p->active; }) == 1); result = true; } else result = false; if (update_visibility) { UpdatePresetVisibility(); ValidatePresetSelections(); } return result; } void GraphicPack2::LoadShaders() { fs::path path(m_filename); for (auto& it : fs::directory_iterator(path.remove_filename())) { if (!is_regular_file(it)) continue; try { const auto& p = it.path(); auto filename = p.filename().wstring(); uint64 shader_base_hash = 0; uint64 shader_aux_hash = 0; wchar_t shader_type[256]{}; if (filename.size() < 256 && swscanf(filename.c_str(), L"%I64x_%I64x_%ls", &shader_base_hash, &shader_aux_hash, shader_type) == 3) { if (shader_type[0] == 'p' && shader_type[1] == 's') m_custom_shaders.emplace_back(LoadShader(p, shader_base_hash, shader_aux_hash, GP_SHADER_TYPE::PIXEL)); else if (shader_type[0] == 'v' && shader_type[1] == 's') m_custom_shaders.emplace_back(LoadShader(p, shader_base_hash, shader_aux_hash, GP_SHADER_TYPE::VERTEX)); else if (shader_type[0] == 'g' && shader_type[1] == 's') m_custom_shaders.emplace_back(LoadShader(p, shader_base_hash, shader_aux_hash, GP_SHADER_TYPE::GEOMETRY)); } else if (filename == L"output.glsl") { std::ifstream file(p); if (!file.is_open()) throw std::runtime_error(fmt::format("can't open graphic pack file: {}", p.filename().string()).c_str()); file.seekg(0, std::ios::end); m_output_shader_source.reserve(file.tellg()); file.seekg(0, std::ios::beg); m_output_shader_source.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); ApplyShaderPresets(m_output_shader_source); } else if (filename == L"upscaling.glsl") { std::ifstream file(p); if (!file.is_open()) throw std::runtime_error(fmt::format("can't open graphic pack file: {}", p.filename().string()).c_str()); file.seekg(0, std::ios::end); m_upscaling_shader_source.reserve(file.tellg()); file.seekg(0, std::ios::beg); m_upscaling_shader_source.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); ApplyShaderPresets(m_upscaling_shader_source); } else if (filename == L"downscaling.glsl") { std::ifstream file(p); if (!file.is_open()) throw std::runtime_error(fmt::format("can't open graphic pack file: {}", p.filename().string()).c_str()); file.seekg(0, std::ios::end); m_downscaling_shader_source.reserve(file.tellg()); file.seekg(0, std::ios::beg); m_downscaling_shader_source.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); ApplyShaderPresets(m_downscaling_shader_source); } } catch (const std::exception& ex) { forceLog_printf("graphicPack: error while loading custom shader: %s", ex.what()); } } } bool GraphicPack2::SetActivePreset(std::string_view name) { return SetActivePreset("", name); } bool GraphicPack2::IsPresetVisible(const PresetPtr& preset) const { if (preset->condition.empty()) return true; try { TExpressionParser p; FillPresetConstants(p); return p.Evaluate(preset->condition) != 0; } catch (const std::exception& ex) { forceLog_printf("error when trying to check visiblity of preset: %s", ex.what()); return false; } } std::optional GraphicPack2::GetPresetVariable(const std::vector& presets, std::string_view var_name) const { // no priority and visibility filter if(m_version < 5) { for (const auto& preset : presets) { const auto it = std::find_if(preset->variables.cbegin(), preset->variables.cend(), [&var_name](auto p) { return p.first == var_name; }); if (it != preset->variables.cend()) return it->second; } return {}; } // visible > none visible > default for (const auto& preset : presets) { if (preset->visible) { const auto it = std::find_if(preset->variables.cbegin(), preset->variables.cend(), [&var_name](auto p) { return p.first == var_name; }); if (it != preset->variables.cend()) return it->second; } } for (const auto& preset : presets) { if (!preset->visible) { const auto it = std::find_if(preset->variables.cbegin(), preset->variables.cend(), [&var_name](auto p) { return p.first == var_name; }); if (it != preset->variables.cend()) return it->second; } } const auto it = std::find_if(m_preset_vars.cbegin(), m_preset_vars.cend(), [&var_name](auto p) { return p.first == var_name; }); if (it != m_preset_vars.cend()) { return it->second; } return {}; } void GraphicPack2::AddConstantsForCurrentPreset(ExpressionParser& ep) { if (m_version < 5) { for (const auto& preset : GetActivePresets()) { for (auto& v : preset->variables) { ep.AddConstant(v.first, v.second.second); } } } else { FillPresetConstants(ep); } } void GraphicPack2::_iterateReplacedFiles(const fs::path& currentPath, std::wstring& internalPath, bool isAOC) { uint64 currentTitleId = CafeSystem::GetForegroundTitleId(); uint64 aocTitleId = (currentTitleId & 0xFFFFFFFFull) | 0x0005000c00000000ull; for (auto& it : fs::recursive_directory_iterator(currentPath)) { if (fs::is_regular_file(it)) { fs::path virtualMountPath = fs::relative(it.path(), currentPath); if (isAOC) { virtualMountPath = fs::path(fmt::format("/vol/aoc{:016x}/", aocTitleId)) / virtualMountPath; } else { virtualMountPath = fs::path("vol/content/") / virtualMountPath; } fscDeviceRedirect_add(virtualMountPath.generic_string(), it.path().generic_string(), m_fs_priority); } } } void GraphicPack2::LoadReplacedFiles() { if (m_patchedFilesLoaded) return; m_patchedFilesLoaded = true; fs::path gfxPackPath(m_filename.c_str()); gfxPackPath = gfxPackPath.remove_filename(); // /content/ fs::path contentPath(gfxPackPath); contentPath.append("content"); std::error_code ec; if (fs::exists(contentPath, ec)) { std::wstring internalPath(L"/vol/content/"); // setup redirections fscDeviceRedirect_map(); _iterateReplacedFiles(contentPath, internalPath, false); } // /aoc/ fs::path aocPath(gfxPackPath); aocPath.append("aoc"); if (fs::exists(aocPath, ec)) { uint64 aocTitleId = CafeSystem::GetForegroundTitleId(); aocTitleId = aocTitleId & 0xFFFFFFFFULL; aocTitleId |= 0x0005000c00000000ULL; wchar_t internalAocPath[128]; swprintf(internalAocPath, sizeof(internalAocPath)/sizeof(wchar_t), L"/aoc/%016llx/", aocTitleId); std::wstring internalPath(internalAocPath); // setup redirections fscDeviceRedirect_map(); _iterateReplacedFiles(aocPath, internalPath, true); } } bool GraphicPack2::Activate() { if (m_activated) return true; // check if gp should be loaded if (m_renderer_api.has_value() && m_renderer_api.value() != g_renderer->GetType()) return false; if (m_gfx_vendor.has_value()) { auto vendor = g_renderer->GetVendor(); if (vendor == GfxVendor::IntelLegacy || vendor == GfxVendor::IntelNoLegacy) vendor = GfxVendor::Intel; if (m_gfx_vendor.value() != vendor) return false; } FileStream* fs_rules = FileStream::openFile2({ m_filename }); if (!fs_rules) return false; std::vector rulesData; fs_rules->extract(rulesData); delete fs_rules; IniParser rules({ (char*)rulesData.data(), rulesData.size()}, boost::nowide::narrow(m_filename)); // load rules try { ExpressionParser parser; AddConstantsForCurrentPreset(parser); while (rules.NextSection()) { //const char* category_name = sPref_currentCategoryName(rules); std::string_view category_name = rules.GetCurrentSectionName(); if (boost::iequals(category_name, "TextureRedefine")) { TextureRule rule{}; ParseRule(parser, rules, "width", &rule.filter_settings.width); ParseRule(parser, rules, "height", &rule.filter_settings.height); ParseRule(parser, rules, "depth", &rule.filter_settings.depth); bool inMem1 = false; if (ParseRule(parser, rules, "inMEM1", &inMem1)) rule.filter_settings.inMEM1 = inMem1 ? TextureRule::FILTER_SETTINGS::MEM1_FILTER::INSIDE : TextureRule::FILTER_SETTINGS::MEM1_FILTER::OUTSIDE; rule.filter_settings.format_whitelist = ParseList(parser, rules, "formats"); rule.filter_settings.format_blacklist = ParseList(parser, rules, "formatsExcluded"); rule.filter_settings.tilemode_whitelist = ParseList(parser, rules, "tilemodes"); rule.filter_settings.tilemode_blacklist = ParseList(parser, rules, "tilemodesExcluded"); ParseRule(parser, rules, "overwriteWidth", &rule.overwrite_settings.width); ParseRule(parser, rules, "overwriteHeight", &rule.overwrite_settings.height); ParseRule(parser, rules, "overwriteDepth", &rule.overwrite_settings.depth); ParseRule(parser, rules, "overwriteFormat", &rule.overwrite_settings.format); float lod_bias; if(ParseRule(parser, rules, "overwriteLodBias", &lod_bias)) rule.overwrite_settings.lod_bias = (sint32)(lod_bias * 64.0f); if(ParseRule(parser, rules, "overwriteRelativeLodBias", &lod_bias)) rule.overwrite_settings.relative_lod_bias = (sint32)(lod_bias * 64.0f); sint32 anisotropyValue; if (ParseRule(parser, rules, "overwriteAnisotropy", &anisotropyValue)) { if (anisotropyValue == 1) rule.overwrite_settings.anistropic_value = 0; else if (anisotropyValue == 2) rule.overwrite_settings.anistropic_value = 1; else if (anisotropyValue == 4) rule.overwrite_settings.anistropic_value = 2; else if (anisotropyValue == 8) rule.overwrite_settings.anistropic_value = 3; else if (anisotropyValue == 16) rule.overwrite_settings.anistropic_value = 4; else cemuLog_log(LogType::Force, fmt::format(L"Invalid value {} for overwriteAnisotropy in graphic pack {}. Only the values 1, 2, 4, 8 or 16 are allowed.", anisotropyValue, m_filename)); } m_texture_rules.emplace_back(rule); } else if (boost::iequals(category_name, "Control")) { ParseRule(parser, rules, "vsyncFrequency", &m_vsync_frequency); } else if (boost::iequals(category_name, "OutputShader")) { auto option_upscale = rules.FindOption("upscaleMagFilter"); if(option_upscale && boost::iequals(*option_upscale, "NearestNeighbor")) m_output_settings.upscale_filter = LatteTextureView::MagFilter::kNearestNeighbor; auto option_downscale = rules.FindOption("NearestNeighbor"); if (option_downscale && boost::iequals(*option_downscale, "NearestNeighbor")) m_output_settings.downscale_filter = LatteTextureView::MagFilter::kNearestNeighbor; } } } catch(const std::exception& ex) { forceLog_printf((char*)ex.what()); return false; } // load shaders LoadShaders(); // load patches LoadPatchFiles(); // enable patch groups EnablePatches(); // load replaced files LoadReplacedFiles(); // set custom vsync if (HasCustomVSyncFrequency()) { sint32 customVsyncFreq = GetCustomVSyncFrequency(); sint32 globalCustomVsyncFreq = 0; if (LatteTiming_getCustomVsyncFrequency(globalCustomVsyncFreq)) { if (customVsyncFreq != globalCustomVsyncFreq) forceLog_printf("rules.txt error: Mismatching vsync frequency %d in graphic pack \'%s\'", customVsyncFreq, GetPath().c_str()); } else { forceLog_printf("Set vsync frequency to %d (graphic pack %s)", customVsyncFreq, GetPath().c_str()); LatteTiming_setCustomVsyncFrequency(customVsyncFreq); } } m_activated = true; return true; } bool GraphicPack2::Deactivate() { if (!m_activated) return false; UnloadPatches(); m_activated = false; m_custom_shaders.clear(); m_texture_rules.clear(); m_output_shader.reset(); m_upscaling_shader.reset(); m_downscaling_shader.reset(); m_output_shader_ud.reset(); m_upscaling_shader_ud.reset(); m_downscaling_shader_ud.reset(); m_output_shader_source = ""; m_upscaling_shader_source = ""; m_downscaling_shader_source = ""; if (HasCustomVSyncFrequency()) { m_vsync_frequency = -1; LatteTiming_disableCustomVsyncFrequency(); } return true; } const std::string* GraphicPack2::FindCustomShaderSource(uint64 shaderBaseHash, uint64 shaderAuxHash, GP_SHADER_TYPE type, bool isVulkanRenderer) { for (const auto& gp : GraphicPack2::GetActiveGraphicPacks()) { const auto it = std::find_if(gp->m_custom_shaders.begin(), gp->m_custom_shaders.end(), [shaderBaseHash, shaderAuxHash, type](const auto& s) { return s.shader_base_hash == shaderBaseHash && s.shader_aux_hash == shaderAuxHash && s.type == type; }); if (it == gp->m_custom_shaders.end()) continue; if(isVulkanRenderer && (*it).isPreVulkanShader) continue; return &it->source; } return nullptr; } std::unordered_map> GraphicPack2::GetCategorizedPresets(std::vector& order) const { order.clear(); std::unordered_map> result; for(const auto& entry : m_presets) { result[entry->category].emplace_back(entry); const auto it = std::find(order.cbegin(), order.cend(), entry->category); if (it == order.cend()) order.emplace_back(entry->category); } return result; } bool GraphicPack2::HasShaders() const { return !GetCustomShaders().empty() || !m_output_shader_source.empty() || !m_upscaling_shader_source.empty() || !m_downscaling_shader_source.empty(); } RendererOutputShader* GraphicPack2::GetOuputShader(bool render_upside_down) { if(render_upside_down) { if (m_output_shader_ud) return m_output_shader_ud.get(); if (!m_output_shader_source.empty()) m_output_shader_ud = std::make_unique(RendererOutputShader::GetOpenGlVertexSource(render_upside_down), m_output_shader_source); return m_output_shader_ud.get(); } else { if (m_output_shader) return m_output_shader.get(); if (!m_output_shader_source.empty()) m_output_shader = std::make_unique(RendererOutputShader::GetOpenGlVertexSource(render_upside_down), m_output_shader_source); return m_output_shader.get(); } } RendererOutputShader* GraphicPack2::GetUpscalingShader(bool render_upside_down) { if (render_upside_down) { if (m_upscaling_shader_ud) return m_upscaling_shader_ud.get(); if (!m_upscaling_shader_source.empty()) m_upscaling_shader_ud = std::make_unique(RendererOutputShader::GetOpenGlVertexSource(render_upside_down), m_upscaling_shader_source); return m_upscaling_shader_ud.get(); } else { if (m_upscaling_shader) return m_upscaling_shader.get(); if (!m_upscaling_shader_source.empty()) m_upscaling_shader = std::make_unique(RendererOutputShader::GetOpenGlVertexSource(render_upside_down), m_upscaling_shader_source); return m_upscaling_shader.get(); } } RendererOutputShader* GraphicPack2::GetDownscalingShader(bool render_upside_down) { if (render_upside_down) { if (m_downscaling_shader_ud) return m_downscaling_shader_ud.get(); if (!m_downscaling_shader_source.empty()) m_downscaling_shader_ud = std::make_unique(RendererOutputShader::GetOpenGlVertexSource(render_upside_down), m_downscaling_shader_source); return m_downscaling_shader_ud.get(); } else { if (m_downscaling_shader) return m_downscaling_shader.get(); if (!m_downscaling_shader_source.empty()) m_downscaling_shader = std::make_unique(RendererOutputShader::GetOpenGlVertexSource(render_upside_down), m_downscaling_shader_source); return m_downscaling_shader.get(); } } std::vector GraphicPack2::GetActivePresets() const { std::vector result; result.reserve(m_presets.size()); std::copy_if(m_presets.cbegin(), m_presets.cend(), std::back_inserter(result), [](const PresetPtr& p) { return p->active; }); return result; } std::vector GraphicPack2::ParseTitleIds(IniParser& rules, const char* option_name) const { std::vector result; auto option_text = rules.FindOption(option_name); if (!option_text) return result; for (auto& token : TokenizeView(*option_text, ',')) { try { result.emplace_back(ConvertString(token, 16)); } catch (const std::invalid_argument&) {} } return result; } void GraphicPack2::ApplyShaderPresets(std::string& shader_source) const { const auto active_presets = GetActivePresets(); const std::regex regex(R"(\$[a-zA-Z\_0-9]+)"); std::smatch match; size_t offset = 0; while (std::regex_search(shader_source.cbegin() + offset, shader_source.cend(), match, regex)) { if (active_presets.empty()) throw std::runtime_error("found variable in shader but no preset is active"); const auto str = match.str(); std::optional var = GetPresetVariable(active_presets, str); if(!var) throw std::runtime_error("using an unknown preset variable in shader"); std::string new_value; if (var->first == kInt) new_value = fmt::format("{}", (int)var->second); else new_value = fmt::format("{:f}", var->second); shader_source.replace(match.position() + offset, match.length(), new_value); offset += match.position() + new_value.length(); } } GraphicPack2::CustomShader GraphicPack2::LoadShader(const fs::path& path, uint64 shader_base_hash, uint64 shader_aux_hash, GP_SHADER_TYPE shader_type) const { CustomShader shader; std::ifstream file(path); if (!file.is_open()) throw std::runtime_error("can't open shader file"); file.seekg(0, std::ios::end); shader.source.reserve(file.tellg()); file.seekg(0, std::ios::beg); shader.source.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); ApplyShaderPresets(shader.source); shader.shader_base_hash = shader_base_hash; shader.shader_aux_hash = shader_aux_hash; shader.type = shader_type; shader.isPreVulkanShader = this->m_version <= 3; return shader; } std::vector> GraphicPack2::GetActiveRAMMappings() { uint64 currentTitleId = CafeSystem::GetForegroundTitleId(); std::vector> v; for (const auto& gp : GraphicPack2::GetGraphicPacks()) { if (!gp->IsEnabled()) continue; if (!gp->ContainsTitleId(currentTitleId)) continue; if (!gp->m_ramMappings.empty()) v.insert(v.end(), gp->m_ramMappings.begin(), gp->m_ramMappings.end()); } std::sort(v.begin(), v.end(), [](const std::pair& a, const std::pair& b) -> bool { return a.first < b.first; }); return v; }