refactor(cleanup): backend initialization logic

This commit is contained in:
PancakeTAS 2025-12-15 16:59:35 +01:00
parent 6bd907516a
commit 883f3d2556
8 changed files with 699 additions and 14 deletions

View file

@ -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})

View file

@ -0,0 +1,244 @@
#include "config.hpp"
#include "lsfg-vk-backend/lsfgvk.hpp"
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#define TOML_ENABLE_FORMATTERS 0
#include <toml.hpp>
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<std::string> activityFromString(const toml::node_view<const toml::node>& val) {
std::vector<std::string> active_in{};
if (const auto& as_str = val.value<std::string>()) {
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<std::string>())
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<std::string>(),
.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<std::string>("unnamed"),
.active_in = activityFromString(tbl["active_in"]),
.gpu = tbl["gpu"].value<std::string>(),
.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<std::string>("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<size_t>(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<GameConf> 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;
}

View file

@ -0,0 +1,71 @@
#pragma once
#include <chrono>
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
namespace lsfgvk::layer {
/// global configuration
struct GlobalConf {
/// optional dll override
std::optional<std::string> 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<std::string> active_in;
/// gpu to use (in case of multiple)
std::optional<std::string> 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<GameConf>& getProfiles() const { return profiles; }
private:
std::filesystem::path path;
std::chrono::time_point<std::chrono::file_clock> timestamp;
bool from_env{};
GlobalConf global;
std::vector<GameConf> profiles;
};
}

View file

@ -0,0 +1,121 @@
#include "detection.hpp"
#include "config.hpp"
#include <array>
#include <cstdlib>
#include <fstream>
#include <optional>
#include <string>
#include <unistd.h>
#include <utility>
#include <vector>
#include <sys/types.h>
using namespace lsfgvk;
using namespace lsfgvk::layer;
namespace {
// try to match a profile by id
std::optional<GameConf> match(const std::vector<GameConf>& 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<char, 4096> buf{};
const ssize_t len = readlink("/proc/self/exe", buf.data(), buf.size() - 1);
if (len > 0) {
buf.at(static_cast<size_t>(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<size_t>(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<std::pair<IdentType, GameConf>> 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;
}

View file

@ -0,0 +1,40 @@
#pragma once
#include "config.hpp"
#include <optional>
#include <string>
#include <utility>
namespace lsfgvk::layer {
/// identification data for a process
struct Identification {
/// optional override name
std::optional<std::string> override;
/// path to exe file
std::string executable;
/// path to exe file when running under wine
std::optional<std::string> 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<std::pair<IdentType, GameConf>> findProfile(
const Configuration& config, const Identification& id);
}

View file

@ -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 <cstdlib>
#include <filesystem>
#include <iostream>
#include <optional>
#include <string>
#include <utility>
#include <vector>
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<const char*> 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<const char*> 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<std::filesystem::path> 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<const std::string&, const std::string&> ids,
const std::optional<std::string>& 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))*/ {
}

View file

@ -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 <optional>
#include <vector>
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<const char*> instanceExtensions() const;
/// required device extensions
/// @return list of extension names
[[nodiscard]] std::vector<const char*> 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<GameConf> 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<lsfgvk::Instance> backend; // lazy due to KhronosGroup/Vulkan-Loader#1739
};
}

View file

@ -1,4 +1,4 @@
#include "layer.hpp"
#include "context/instance.hpp"
#include "lsfg-vk-common/vulkan/vulkan.hpp"
#include <algorithm>
@ -55,10 +55,10 @@ namespace {
// global layer info initialized at layer negotiation
struct LayerInfo {
layer::Layer layer; //!< basic layer info
std::unordered_map<std::string, PFN_vkVoidFunction> 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<uint32_t>(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<uint32_t>(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<PFN_vkVoidFunction>(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";