diff --git a/lsfg-vk-layer/CMakeLists.txt b/lsfg-vk-layer/CMakeLists.txt index 34c4a8a..9c1ac7a 100644 --- a/lsfg-vk-layer/CMakeLists.txt +++ b/lsfg-vk-layer/CMakeLists.txt @@ -1,8 +1,8 @@ set(LAYER_SOURCES - "src/config.cpp" - "src/detection.cpp" - "src/entrypoint.cpp" - "src/layer.cpp") + "src/configuration/config.cpp" + "src/configuration/detection.cpp" + "src/context/instance.cpp" + "src/entrypoint.cpp") add_library(lsfg-vk-layer SHARED ${LAYER_SOURCES}) diff --git a/lsfg-vk-layer/src/configuration/config.cpp b/lsfg-vk-layer/src/configuration/config.cpp new file mode 100644 index 0000000..cae857b --- /dev/null +++ b/lsfg-vk-layer/src/configuration/config.cpp @@ -0,0 +1,244 @@ +#include "config.hpp" +#include "lsfg-vk-backend/lsfgvk.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#define TOML_ENABLE_FORMATTERS 0 +#include + +using namespace lsfgvk::layer; + +namespace { + const char* DEFAULT_CONFIG = R"(version = 2 + +[global] +# dll = '/media/games/Lossless Scaling/Lossless.dll' # if you don't have LS in the default location +allow_fp16 = true # this will improve give a MASSIVE performance boost on AMD, but be super slow on older (!) NVIDIA GPUs + +[[profile]] +name = "4x FG / 85% [Performance]" +active_in = [ # see the wiki for more info + 'vkcube', + 'vkcubepp' +] +gpu = 'NVIDIA GeForce RTX 5080' # see the wiki for more info +multiplier = 4 +flow_scale = 0.85 +performance_mode = true +pacing = 'none' # see the wiki for more info + +[[profile]] +name = "2x FG / 100%" +active_in = 'GenshinImpact.exe' +gpu = 'NVIDIA GeForce RTX 5080' +multiplier = 2 +)"; + + /// parse an activity array from toml value + std::vector activityFromString(const toml::node_view& val) { + std::vector active_in{}; + + if (const auto& as_str = val.value()) { + active_in.push_back(*as_str); + } + + if (const auto& as_arr = val.as_array()) { + for (const auto& item : *as_arr) { + if (const auto& item_str = item.value()) + active_in.push_back(*item_str); + } + } + + return active_in; + } + /// parse a pacing method from string + Pacing parcingFromString(const std::string& str) { + if (str == "none") + return Pacing::None; + throw lsfgvk::error("unknown pacing method: " + str); + } + /// try to find the config + std::filesystem::path findPath() { + // always honor LSFGVK_CONFIG if set + const char* envPath = std::getenv("LSFGVK_CONFIG"); + if (envPath && *envPath != '\0') + return{envPath}; + + // then check the XDG overriden location + const char* xdgPath = std::getenv("XDG_CONFIG_HOME"); + if (xdgPath && *xdgPath != '\0') + return std::filesystem::path(xdgPath) + / "lsfg-vk" / "conf.toml"; + + // fallback to typical user home + const char* homePath = std::getenv("HOME"); + if (homePath && *homePath != '\0') + return std::filesystem::path(homePath) + / ".config" / "lsfg-vk" / "conf.toml"; + + // finally, use system-wide config + return "/etc/lsfg-vk/conf.toml"; + } + /// parse the global configuration + GlobalConf parseGlobalConf(const toml::table& tbl) { + const GlobalConf conf{ + .dll = tbl["dll"].value(), + .allow_fp16 = tbl["allow_fp16"].value_or(true) + }; + + if (conf.dll && !std::filesystem::exists(*conf.dll)) + throw lsfgvk::error("path to dll is invalid"); + + return conf; + } + /// parse a game profile configuration + GameConf parseGameConf(const toml::table& tbl) { + const GameConf conf{ + .name = tbl["name"].value_or("unnamed"), + .active_in = activityFromString(tbl["active_in"]), + .gpu = tbl["gpu"].value(), + .multiplier = tbl["multiplier"].value_or(2U), + .flow_scale = tbl["flow_scale"].value_or(1.0F), + .performance_mode = tbl["performance_mode"].value_or(false), + .pacing = parcingFromString(tbl["pacing"].value_or("none")) + }; + + if (conf.multiplier <= 1) + throw lsfgvk::error("multiplier must be greater than 1"); + if (conf.flow_scale < 0.25F || conf.flow_scale > 1.0F) + throw lsfgvk::error("flow_scale must be between 0.25 and 1.0"); + + return conf; + } + /// parse the global configuration from the environment + GlobalConf parseGlobalConfFromEnv() { + GlobalConf conf{ + .dll = std::nullopt, + .allow_fp16 = true + }; + + const char* dll = std::getenv("LSFGVK_DLL_PATH"); + if (dll && *dll != '\0') + conf.dll = std::string(dll); + const char* no_fp16 = std::getenv("LSFGVK_NO_FP16"); + if (no_fp16 && *no_fp16 != '\0') + conf.allow_fp16 = std::string(no_fp16) != "1"; + + if (conf.dll && !std::filesystem::exists(*conf.dll)) + throw lsfgvk::error("path to dll is invalid"); + + return conf; + } + /// parse a game profile configuration from the environment + GameConf parseGameConfFromEnv() { + GameConf conf{ + .name = "(environment)", + .active_in = {}, + .gpu = std::nullopt, + + .multiplier = 2, + .flow_scale = 1.0F, + .performance_mode = false, + .pacing = Pacing::None + }; + + const char* gpu = std::getenv("LSFGVK_GPU"); + if (gpu) conf.gpu = std::string(gpu); + const char* multiplier = std::getenv("LSFGVK_MULTIPLIER"); + if (multiplier) conf.multiplier = static_cast(std::stoul(multiplier)); + const char* flow_scale = std::getenv("LSFGVK_FLOW_SCALE"); + if (flow_scale) conf.flow_scale = std::stof(flow_scale); + const char* performance = std::getenv("LSFGVK_PERFORMANCE_MODE"); + if (performance) conf.performance_mode = std::string(performance) == "1"; + const char* pacing = std::getenv("LSFGVK_PACING"); + if (pacing) conf.pacing = parcingFromString(std::string(pacing)); + + if (conf.multiplier <= 1) + throw lsfgvk::error("multiplier must be greater than 1"); + if (conf.flow_scale < 0.25F || conf.flow_scale > 1.0F) + throw lsfgvk::error("flow_scale must be between 0.25 and 1.0"); + + return conf; + } +} + +Configuration::Configuration() : + path(findPath()), + from_env(std::getenv("LSFGVK_ENV") != nullptr) { + if (std::filesystem::exists(this->path) || this->from_env) + return; + + try { + std::filesystem::create_directories(this->path.parent_path()); + if (!std::filesystem::exists(this->path.parent_path())) + throw lsfgvk::error("unable to create configuration directory"); + + std::ofstream ofs(this->path); + if (!ofs.is_open()) + throw lsfgvk::error("unable to create default configuration file"); + + ofs << DEFAULT_CONFIG; + ofs.close(); + } catch (const std::filesystem::filesystem_error& e) { + throw lsfgvk::error("unable to create default configuration file", e); + } +} + +bool Configuration::tick() { + if (this->from_env) { + if (this->profiles.empty()) { + this->global = parseGlobalConfFromEnv(); + this->profiles.push_back(parseGameConfFromEnv()); + return true; + } + + return false; // no need to tick if from env + } + + // check for updates + try { + auto time = std::filesystem::last_write_time(this->path); + if (time == this->timestamp) + return false; + + this->timestamp = time; + } catch (const std::filesystem::filesystem_error& e) { + throw lsfgvk::error("unable to access configuration file", e); + } + + // parse configuration + GlobalConf global{}; + std::vector profiles{}; + + toml::table tbl; + try { + tbl = toml::parse_file(this->path.string()); + } catch (const toml::parse_error& e) { + throw lsfgvk::error("unable to parse configuration", e); + } + + auto vrs = tbl["version"]; + if (!vrs || !vrs.is_integer() || *vrs.as_integer() != 2) + throw lsfgvk::error("unsupported configuration version"); + + auto gbl = tbl["global"]; + if (gbl && gbl.is_table()) { + global = parseGlobalConf(*gbl.as_table()); + } + + auto pfls = tbl["profile"]; + if (pfls && pfls.is_array_of_tables()) { + for (const auto& pfl : *pfls.as_array()) + profiles.push_back(parseGameConf(*pfl.as_table())); + } + + this->global = std::move(global); + this->profiles = std::move(profiles); + return true; +} diff --git a/lsfg-vk-layer/src/configuration/config.hpp b/lsfg-vk-layer/src/configuration/config.hpp new file mode 100644 index 0000000..c8afee8 --- /dev/null +++ b/lsfg-vk-layer/src/configuration/config.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace lsfgvk::layer { + + /// global configuration + struct GlobalConf { + /// optional dll override + std::optional dll; + /// should fp16 be allowed + bool allow_fp16; + }; + + /// pacing methods + enum class Pacing { + /// do not perform any pacing (vsync+novrr) + None + }; + + /// game profile configuration + struct GameConf { + /// name of the profile + std::string name; + /// optional activation string/array + std::vector active_in; + /// gpu to use (in case of multiple) + std::optional gpu; + /// multiplier for frame generation + size_t multiplier; + /// non-inverted flow scale + float flow_scale; + /// use performance mode + bool performance_mode; + /// pacing method + Pacing pacing; + }; + + /// automatically updating configuration + class Configuration { + public: + /// create a new configuration + /// @throws lsfgvk::error on failure + Configuration(); + + /// reload the configuration from disk if the file has changed + /// @throws lsfgvk::error on failure + /// @return true if the configuration was reloaded + bool tick(); + + /// get the global configuration + /// @return global configuration + [[nodiscard]] const GlobalConf& getGlobalConf() const { return global; } + + /// get the game profiles + /// @return list of game profiles + [[nodiscard]] const std::vector& getProfiles() const { return profiles; } + private: + std::filesystem::path path; + std::chrono::time_point timestamp; + bool from_env{}; + + GlobalConf global; + std::vector profiles; + }; + +} diff --git a/lsfg-vk-layer/src/configuration/detection.cpp b/lsfg-vk-layer/src/configuration/detection.cpp new file mode 100644 index 0000000..e4144d5 --- /dev/null +++ b/lsfg-vk-layer/src/configuration/detection.cpp @@ -0,0 +1,121 @@ +#include "detection.hpp" +#include "config.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace lsfgvk; +using namespace lsfgvk::layer; + +namespace { + // try to match a profile by id + std::optional match(const std::vector& profiles, const std::string& id) { + for (const auto& profile : profiles) + for (const auto& activation : profile.active_in) + if (activation == id) + return profile; + return std::nullopt; + } +} + +Identification layer::identify() { + Identification id{}; + + // fetch LSFGVK_PROFILE + const char* override = std::getenv("LSFGVK_PROFILE"); + if (override && *override != '\0') + id.override = std::string(override); + + // fetch process exe path + std::array buf{}; + const ssize_t len = readlink("/proc/self/exe", buf.data(), buf.size() - 1); + if (len > 0) { + buf.at(static_cast(len)) = '\0'; + id.executable = std::string(buf.data()); + } + + // if running under wine, fetch the actual exe path + if (id.executable.find("wine") != std::string::npos + || id.executable.find("proton") != std::string::npos) { + + std::ifstream maps("/proc/self/maps"); + std::string line; + while (maps.is_open() && std::getline(maps, line)) { + if (!line.ends_with(".exe")) + continue; + + size_t pos = line.find_first_of('/'); + if (pos == std::string::npos) { + pos = line.find_last_of(' '); + if (pos == std::string::npos) + continue; + pos += 1; // skip space + } + + const std::string wine_executable = line.substr(pos); + if (wine_executable.empty()) + continue; + + id.wine_executable = wine_executable; + break; + } + } + + // fetch process name + std::ifstream comm("/proc/self/comm"); + if (comm.is_open()) { + comm.read(buf.data(), buf.size() - 1); + buf.at(static_cast(comm.gcount())) = '\0'; + + id.process_name = std::string(buf.data()); + if (id.process_name.back() == '\n') + id.process_name.pop_back(); + } + + return id; +} + +std::optional> layer::findProfile( + const Configuration& config, const Identification& id) { + const auto& profiles = config.getProfiles(); + + // check for the environment option first + if (std::getenv("LSFGVK_ENV") != nullptr) + return std::make_pair(IdentType::OVERRIDE, config.getProfiles().front()); + + // then override first + if (id.override.has_value()) { + const auto profile = match(profiles, id.override.value()); + if (profile.has_value()) + return std::make_pair(IdentType::OVERRIDE, profile.value()); + } + + // then check executable + const auto exe_profile = match(profiles, id.executable); + if (exe_profile.has_value()) + return std::make_pair(IdentType::EXECUTABLE, exe_profile.value()); + + // if present, check wine executable next + if (id.wine_executable.has_value()) { + const auto wine_profile = match(profiles, id.wine_executable.value()); + if (wine_profile.has_value()) + return std::make_pair(IdentType::WINE_EXECUTABLE, wine_profile.value()); + } + + // finally, fallback to process name + if (!id.process_name.empty()) { + const auto proc_profile = match(profiles, id.process_name); + if (proc_profile.has_value()) + return std::make_pair(IdentType::PROCESS_NAME, proc_profile.value()); + } + + return std::nullopt; +} diff --git a/lsfg-vk-layer/src/configuration/detection.hpp b/lsfg-vk-layer/src/configuration/detection.hpp new file mode 100644 index 0000000..5ef9f32 --- /dev/null +++ b/lsfg-vk-layer/src/configuration/detection.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "config.hpp" + +#include +#include +#include + +namespace lsfgvk::layer { + + /// identification data for a process + struct Identification { + /// optional override name + std::optional override; + /// path to exe file + std::string executable; + /// path to exe file when running under wine + std::optional wine_executable; + /// traditional process name (e.g. GameThread) + std::string process_name; + }; + + /// enum describing which identification method was used + enum class IdentType { + OVERRIDE, // identified by override + EXECUTABLE, // identified by executable path + WINE_EXECUTABLE, // identified by wine executable path + PROCESS_NAME // identified by process name + }; + + /// identify the current process + Identification identify(); + + /// find a profile for the current process + /// @param config configuration to search in + /// @param id identification data + /// @return ident pair if found + std::optional> findProfile( + const Configuration& config, const Identification& id); +} diff --git a/lsfg-vk-layer/src/context/instance.cpp b/lsfg-vk-layer/src/context/instance.cpp new file mode 100644 index 0000000..14964de --- /dev/null +++ b/lsfg-vk-layer/src/context/instance.cpp @@ -0,0 +1,149 @@ +#include "instance.hpp" +#include "../configuration/config.hpp" +#include "../configuration/detection.hpp" +#include "lsfg-vk-backend/lsfgvk.hpp" +#include "lsfg-vk-common/vulkan/vulkan.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace lsfgvk; +using namespace lsfgvk::layer; + +Root::Root() : identification(identify()) { + this->tick(); + + if (!this->profile.has_value()) + return; + + std::cerr << "lsfg-vk: using profile with name '" << this->profile->name << "' "; + switch (this->identType) { + case IdentType::OVERRIDE: + std::cerr << "(identified via override)\n"; + break; + case IdentType::EXECUTABLE: + std::cerr << "(identified via executable)\n"; + break; + case IdentType::WINE_EXECUTABLE: + std::cerr << "(identified via wine executable)\n"; + break; + case IdentType::PROCESS_NAME: + std::cerr << "(identified via process name)\n"; + break; + } +} + +bool Root::tick() { + auto res = this->config.tick(); + if (!res) + return false; + + // try to find a profile + const auto& detec = findProfile(this->config, identification); + if (!detec.has_value()) + return this->profile.has_value(); + + this->identType = detec->first; + this->profile = detec->second; + return true; +} + +std::vector Root::instanceExtensions() const { + if (!this->profile.has_value()) + return {}; + + return { + "VK_KHR_get_physical_device_properties2", + "VK_KHR_external_memory_capabilities", + "VK_KHR_external_semaphore_capabilities" + }; +} + +std::vector Root::deviceExtensions() const { + if (!this->profile.has_value()) + return {}; + + return { + "VK_KHR_external_memory", + "VK_KHR_external_memory_fd", + "VK_KHR_external_semaphore", + "VK_KHR_external_semaphore_fd", + "VK_KHR_timeline_semaphore" + }; +} + +namespace { + std::filesystem::path findShaderDll() { + const std::vector FRAGMENTS{{ + ".local/share/Steam/steamapps/common", + ".steam/steam/steamapps/common", + ".steam/debian-installation/steamapps/common", + ".var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common", + "snap/steam/common/.local/share/Steam/steamapps/common" + }}; + + // check XDG overridden location + const char* xdgPath = std::getenv("XDG_DATA_HOME"); + if (xdgPath && *xdgPath != '\0') { + auto base = std::filesystem::path(xdgPath); + + for (const auto& frag : FRAGMENTS) { + auto full = base / frag / "Lossless Scaling" / "Lossless.dll"; + if (std::filesystem::exists(full)) + return full; + } + } + + // check home directory + const char* homePath = std::getenv("HOME"); + if (homePath && *homePath != '\0') { + auto base = std::filesystem::path(homePath); + + for (const auto& frag : FRAGMENTS) { + auto full = base / frag / "Lossless Scaling" / "Lossless.dll"; + if (std::filesystem::exists(full)) + return full; + } + } + + // fallback to same directory + auto local = std::filesystem::current_path() / "Lossless.dll"; + if (std::filesystem::exists(local)) + return local; + + throw lsfgvk::error("unable to locate Lossless.dll, please set the path in the configuration"); + } + + lsfgvk::Instance createBackend(const Root& root) { + const auto& conf = root.conf(); + const auto& profile = root.active(); + if (!profile.has_value()) // should not happen + throw lsfgvk::error("no profile active"); + + return { + [profile = *profile]( + const std::string& deviceName, + std::pair ids, + const std::optional& pci + ) { + if (!profile.gpu) + return true; + + return (deviceName == *profile.gpu) + || (ids.first + ":" + ids.second == *profile.gpu) + || (pci && *pci == *profile.gpu); + }, + conf.dll.value_or(findShaderDll()), + conf.allow_fp16 + }; + } +} + +layer::Instance::Instance(const Root& root, vk::Vulkan vk) + : vk(std::move(vk))/*, backend(createBackend(layer))*/ { +} diff --git a/lsfg-vk-layer/src/context/instance.hpp b/lsfg-vk-layer/src/context/instance.hpp new file mode 100644 index 0000000..24dc7a7 --- /dev/null +++ b/lsfg-vk-layer/src/context/instance.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "../configuration/config.hpp" +#include "../configuration/detection.hpp" +#include "lsfg-vk-backend/lsfgvk.hpp" +#include "lsfg-vk-common/helpers/pointers.hpp" +#include "lsfg-vk-common/vulkan/vulkan.hpp" + +#include +#include + +namespace lsfgvk::layer { + + /// root of the lsfg-vk layer + class Root { + public: + /// create a new root + /// @throws lsfgvk::error on failure + Root(); + + /// get the global configuration + /// @return configuration + [[nodiscard]] const auto& conf() const { return config.getGlobalConf(); } + /// get the active profile + /// @return game configuration + [[nodiscard]] const auto& active() const { return profile; } + + /// required instance extensions + /// @return list of extension names + [[nodiscard]] std::vector instanceExtensions() const; + /// required device extensions + /// @return list of extension names + [[nodiscard]] std::vector deviceExtensions() const; + + /// tick the root + /// @throws lsfgvk::error on failure + /// @return true if profile changed + bool tick(); + private: + Configuration config; + Identification identification; + + IdentType identType{}; // type used to deduce the profile + std::optional profile; + }; + + + /// instance of the lsfg-vk layer on a VkInstance/VkDevice pair. + class Instance { + public: + /// create a new layer instance + /// @param root root of the layer + /// @param vk vulkan instance + Instance(const Root& root, vk::Vulkan vk); + private: + vk::Vulkan vk; + ls::lazy backend; // lazy due to KhronosGroup/Vulkan-Loader#1739 + }; + +} diff --git a/lsfg-vk-layer/src/entrypoint.cpp b/lsfg-vk-layer/src/entrypoint.cpp index 9a3c786..3cca3f5 100644 --- a/lsfg-vk-layer/src/entrypoint.cpp +++ b/lsfg-vk-layer/src/entrypoint.cpp @@ -1,4 +1,4 @@ -#include "layer.hpp" +#include "context/instance.hpp" #include "lsfg-vk-common/vulkan/vulkan.hpp" #include @@ -55,10 +55,10 @@ namespace { // global layer info initialized at layer negotiation struct LayerInfo { - layer::Layer layer; //!< basic layer info std::unordered_map map; //!< function pointer override map - PFN_vkGetInstanceProcAddr GetInstanceProcAddr; + + layer::Root root; }* layer_info; // create instance @@ -106,7 +106,7 @@ namespace { auto extensions = add_extensions( info->ppEnabledExtensionNames, info->enabledExtensionCount, - layer_info->layer.instanceExtensions()); + layer_info->root.instanceExtensions()); VkInstanceCreateInfo newInfo = *info; newInfo.enabledExtensionCount = static_cast(extensions.size()); @@ -183,7 +183,7 @@ namespace { auto extensions = add_extensions( info->ppEnabledExtensionNames, info->enabledExtensionCount, - layer_info->layer.deviceExtensions()); + layer_info->root.deviceExtensions()); VkDeviceCreateInfo newInfo = *info; newInfo.enabledExtensionCount = static_cast(extensions.size()); @@ -205,7 +205,7 @@ namespace { .handle = *device, .funcs = vk::initVulkanDeviceFuncs(instance_info->funcs, *device), .layer = layer::Instance( - layer_info->layer, + layer_info->root, vk::Vulkan( instance_info->handle, *device, physdev, instance_info->funcs, @@ -316,12 +316,11 @@ VkResult vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface* pVers // load the layer configuration try { - layer::Layer layer{}; - if (!layer.active()) // skip inactive + layer::Root root{}; + if (!root.active()) // skip inactive return VK_ERROR_INITIALIZATION_FAILED; layer_info = new LayerInfo{ - .layer = std::move(layer), .map = { #define VKPTR(name) reinterpret_cast(name) { "vkCreateInstance", VKPTR(myvkCreateInstance) }, @@ -329,7 +328,8 @@ VkResult vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface* pVers { "vkDestroyDevice", VKPTR(myvkDestroyDevice) }, { "vkDestroyInstance", VKPTR(myvkDestroyInstance) } #undef VKPTR - } + }, + .root = std::move(root) }; } catch (const std::exception& e) { std::cerr << "lsfg-vk: something went wrong during lsfg-vk layer initialization:\n";