refactor(cleanup): custom cli (including benchmark) & fixup

This commit is contained in:
PancakeTAS 2025-12-24 19:54:50 +01:00
parent adffd92357
commit 6236dcf8fc
No known key found for this signature in database
26 changed files with 851 additions and 368 deletions

View file

@ -40,9 +40,10 @@ jobs:
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=./target \
-DCMAKE_CXX_COMPILER=clang++ \
-DLSFGVK_BUILD_VULKAN_LAYER=On \
-DLSFGVK_BUILD_USER_INTERFACE=On \
-DLSFGVK_INSTALL_XDG_FILES=On \
-DLSFGVK_BUILD_VK_LAYER=ON \
-DLSFGVK_BUILD_UI=ON \
-DLSFGVK_BUILD_CLI=ON \
-DLSFGVK_INSTALL_XDG_FILES=ON \
-DLSFGVK_LAYER_LIBRARY_PATH="../../../lib/liblsfg-vk-layer.so"
- name: Build with Ninja
run: |

View file

@ -59,9 +59,10 @@ jobs:
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_INSTALL_PREFIX=./target \
-DCMAKE_CXX_COMPILER=clang++ \
-DLSFGVK_BUILD_VULKAN_LAYER=On \
-DLSFGVK_BUILD_USER_INTERFACE=On \
-DLSFGVK_INSTALL_XDG_FILES=On \
-DLSFGVK_BUILD_VK_LAYER=ON \
-DLSFGVK_BUILD_UI=ON \
-DLSFGVK_BUILD_CLI=ON \
-DLSFGVK_INSTALL_XDG_FILES=ON \
-DLSFGVK_LAYER_LIBRARY_PATH="../../../lib/liblsfg-vk-layer.so"
- name: Build with Ninja
run: |

View file

@ -3,12 +3,13 @@ project(lsfg-vk LANGUAGES CXX)
include(GNUInstallDirs)
# === READ HERE FOR BUILD OPTIONS ===
option(LSFGVK_BUILD_VULKAN_LAYER "Build the Vulkan layer" ON)
option(LSFGVK_BUILD_USER_INTERFACE "Build the user interface" OFF)
option(LSFGVK_BUILD_DEBUG_TOOL "Build the debug tool" OFF)
option(LSFGVK_INSTALL_DEVELOP "Install development libraries and headers" OFF)
option(LSFGVK_INSTALL_XDG_FILES "Install the application icon and desktop files" OFF)
option(LSFGVK_LAYER_LIBRARY_PATH "Change where Vulkan searches for the layer library" liblsfg-vk-layer.so)
option(LSFGVK_BUILD_VK_LAYER "Build the Vulkan layer" ON)
option(LSFGVK_BUILD_UI "Build the user interface" OFF)
option(LSFGVK_BUILD_CLI "Build the command line interface" ON)
option(LSFGVK_INSTALL_DEVELOP "Install development libraries and headers" OFF)
option(LSFGVK_INSTALL_XDG_FILES "Install the application icon and desktop files" OFF)
option(LSFGVK_LAYER_LIBRARY_PATH "Change where Vulkan searches for the layer library" liblsfg-vk-layer.so)
option(LSFGVK_TESTING_RENDERDOC "Enable RenderDoc integration for testing purposes" OFF)
# === READ HERE FOR BUILD OPTIONS ===
set(CMAKE_CXX_STANDARD 20)
@ -43,18 +44,18 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
endif()
endif()
if(LSFGVK_BUILD_DEBUG_TOOL)
add_compile_definitions(LSFGVK__RENDERDOC_INTEGRATION)
if(LSFGVK_TESTING_RENDERDOC)
add_compile_definitions(LSFGVK_TESTING_RENDERDOC)
endif()
add_subdirectory(lsfg-vk-common)
add_subdirectory(lsfg-vk-backend)
if(LSFGVK_BUILD_VULKAN_LAYER)
if(LSFGVK_BUILD_VK_LAYER)
add_subdirectory(lsfg-vk-layer)
endif()
if(LSFGVK_BUILD_USER_INTERFACE)
if(LSFGVK_BUILD_UI)
add_subdirectory(lsfg-vk-ui)
endif()
if(LSFGVK_BUILD_DEBUG_TOOL)
add_subdirectory(lsfg-vk-debug)
if(LSFGVK_BUILD_CLI)
add_subdirectory(lsfg-vk-cli)
endif()

View file

@ -20,8 +20,11 @@ modules:
buildsystem: cmake-ninja
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=On
- -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
- -DCMAKE_CXX_COMPILER=clang++
- -DLSFGVK_BUILD_VK_LAYER=ON
- -DLSFGVK_BUILD_UI=OFF
- -DLSFGVK_BUILD_CLI=OFF
- -DLSFGVK_LAYER_LIBRARY_PATH=/usr/lib/extensions/vulkan/lsfgvk/lib/liblsfg-vk-layer.so
sources:
- type: dir

View file

@ -20,8 +20,11 @@ modules:
buildsystem: cmake-ninja
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=On
- -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
- -DCMAKE_CXX_COMPILER=clang++
- -DLSFGVK_BUILD_VK_LAYER=ON
- -DLSFGVK_BUILD_UI=OFF
- -DLSFGVK_BUILD_CLI=OFF
- -DLSFGVK_LAYER_LIBRARY_PATH=/usr/lib/extensions/vulkan/lsfgvk/lib/liblsfg-vk-layer.so
sources:
- type: dir

View file

@ -20,8 +20,11 @@ modules:
buildsystem: cmake-ninja
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=On
- -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
- -DCMAKE_CXX_COMPILER=clang++
- -DLSFGVK_BUILD_VK_LAYER=ON
- -DLSFGVK_BUILD_UI=OFF
- -DLSFGVK_BUILD_CLI=OFF
- -DLSFGVK_LAYER_LIBRARY_PATH=/usr/lib/extensions/vulkan/lsfgvk/lib/liblsfg-vk-layer.so
sources:
- type: dir

View file

@ -19,9 +19,10 @@ modules:
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INSTALL_PREFIX=/app
- -DLSFGVK_BUILD_VULKAN_LAYER=Off
- -DLSFGVK_BUILD_USER_INTERFACE=On
- -DLSFGVK_INSTALL_XDG_FILES=On
- -DLSFGVK_BUILD_VK_LAYER=OFF
- -DLSFGVK_BUILD_UI=ON
- -DLSFGVK_BUILD_CLI=OFF
- -DLSFGVK_INSTALL_XDG_FILES=ON
sources:
- type: dir
path: ../../..

View file

@ -1,15 +0,0 @@
#pragma once
#include <cstdint>
namespace Benchmark {
///
/// Run the benchmark.
///
/// @param width The width of the benchmark.
/// @param height The height of the benchmark.
///
[[noreturn]] void run(uint32_t width, uint32_t height);
}

View file

@ -42,7 +42,7 @@
#include <vulkan/vulkan_core.h>
#ifdef LSFGVK__RENDERDOC_INTEGRATION
#ifdef LSFGVK_TESTING_RENDERDOC
#include <renderdoc_app.h>
#include <dlfcn.h>
#endif
@ -72,7 +72,7 @@ namespace lsfgvk::backend {
/// get the shader registry
/// @return the shader registry
[[nodiscard]] const auto& getShaderRegistry() const { return this->shaders; }
#ifdef LSFGVK__RENDERDOC_INTEGRATION
#ifdef LSFGVK_TESTING_RENDERDOC
/// get the RenderDoc API
/// @return the RenderDoc API
[[nodiscard]] const auto& getRenderDocAPI() const { return this->renderdoc; }
@ -81,7 +81,7 @@ namespace lsfgvk::backend {
vk::Vulkan vk;
ShaderRegistry shaders;
#ifdef LSFGVK__RENDERDOC_INTEGRATION
#ifdef LSFGVK_TESTING_RENDERDOC
std::optional<RENDERDOC_API_1_6_0> renderdoc;
#endif
};
@ -232,7 +232,7 @@ namespace {
throw backend::error("Unable to build shader registry", e);
}
}
#ifdef LSFGVK__RENDERDOC_INTEGRATION
#ifdef LSFGVK_TESTING_RENDERDOC
/// load RenderDoc integration
std::optional<RENDERDOC_API_1_6_0> loadRenderDocIntegration() {
void* module = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
@ -260,7 +260,7 @@ InstanceImpl::InstanceImpl(vk::PhysicalDeviceSelector selectPhysicalDevice,
: vk(createVulkanInstance(selectPhysicalDevice)),
shaders(createShaderRegistry(this->vk, shaderDllPath,
allowLowPrecision && vk.supportsFP16())) {
#ifdef LSFGVK__RENDERDOC_INTEGRATION
#ifdef LSFGVK_TESTING_RENDERDOC
this->renderdoc = loadRenderDocIntegration();
#endif
vk.persistPipelineCache(); // will silently fail
@ -540,8 +540,8 @@ ContextImpl::ContextImpl(const InstanceImpl& instance,
cmdbuf.submit(ctx.vk); // wait for completion
}
void Instance::scheduleFrames(Context& context) {
#ifdef LSFGVK__RENDERDOC_INTEGRATION
void Instance::scheduleFrames(Context& context) { // NOLINT (static)
#ifdef LSFGVK_TESTING_RENDERDOC
const auto& impl = this->m_impl;
if (impl->getRenderDocAPI()) {
impl->getRenderDocAPI()->StartFrameCapture(
@ -554,7 +554,7 @@ void Instance::scheduleFrames(Context& context) {
} catch (const std::exception& e) {
throw backend::error("Unable to schedule frames", e);
}
#ifdef LSFGVK__RENDERDOC_INTEGRATION
#ifdef LSFGVK_TESTING_RENDERDOC
if (impl->getRenderDocAPI()) {
impl->getVulkan().df().DeviceWaitIdle(impl->getVulkan().dev());
impl->getRenderDocAPI()->EndFrameCapture(

View file

@ -16,14 +16,17 @@ Checks:
- "-readability-math-missing-parentheses"
- "-readability-named-parameter"
- "-bugprone-easily-swappable-parameters"
- "-misc-non-private-member-variables-in-classes"
# configure modernization
- "modernize-*"
- "-modernize-use-trailing-return-type"
- "-modernize-use-designated-initializers"
# configure cppcoreguidelines
- "cppcoreguidelines-*"
- "-cppcoreguidelines-avoid-magic-numbers"
- "-cppcoreguidelines-pro-type-reinterpret-cast"
- "-cppcoreguidelines-macro-usage"
- "-cppcoreguidelines-pro-bounds-pointer-arithmetic"
# disable slow and pointless checks
- "-modernize-use-std-numbers"
- "-modernize-type-traits"

View file

@ -0,0 +1,18 @@
set(CLI_SOURCES
"src/tools/benchmark.cpp"
"src/tools/debug.cpp"
"src/tools/validate.cpp"
"src/main.cpp")
add_executable(lsfg-vk-cli ${CLI_SOURCES})
target_link_libraries(lsfg-vk-cli
PUBLIC lsfg-vk-common
PUBLIC lsfg-vk-backend)
target_compile_options(lsfg-vk-cli PRIVATE
-Wno-unknown-warning-option
-Wno-unsafe-buffer-usage)
install(TARGETS lsfg-vk-cli
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")

225
lsfg-vk-cli/src/main.cpp Normal file
View file

@ -0,0 +1,225 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "tools/benchmark.hpp"
#include "tools/debug.hpp"
#include "tools/validate.hpp"
#include <array>
#include <filesystem>
#include <cstdlib>
#include <iostream>
#include <optional>
#include <string>
#include <getopt.h> // NOLINT
#include <bits/getopt_core.h>
#include <bits/getopt_ext.h>
using namespace lsfgvk::cli;
namespace {
/// print usage information
void usage(const std::string& prog) {
std::cerr <<
R"(Validate, benchmark, and debug lsfg-vk.
USAGE:
)" << prog << R"( <COMMAND> [OPTIONS] [ARGS]
COMMANDS:
validate Validate a configuration file
benchmark Run a benchmark
debug Run lsfg-vk on a set of images
SUBCOMMAND OPTIONS:
validate
-c, --config <PATH> Optional path to the configuration file
benchmark & debug
-d, --dll <PATH> Path to Lossless.dll
-a, --allow-fp16 Allow FP16 acceleration
-w, --width <INT> Width of the input frames
-h, --height <INT> Height of the input frames
-f, --flow <FLOAT> Flow scale
-m, --multiplier <INT> Multiplier
-p, --performance-mode Use performance mode
-g, --gpu <STRING> GPU to use
benchmark
-t, --duration <SECONDS> Benchmark duration in seconds
debug
<folder> Path to the debug frames)" << '\n';
}
/// parse the validate command options
[[noreturn]] void on_validate(int argc, char** argv) {
validate::Options opts{};
const std::array<option, 3> GETOPT {{
{ "config", required_argument, nullptr, 'c' },
{ nullptr, no_argument, nullptr, 0 }
}};
int c{0};
while ((c = getopt_long(argc, argv, "c:", GETOPT.data(), nullptr)) != -1) {
switch (c) {
case 'c':
opts.config.emplace(optarg);
break;
case '?':
default:
usage(*argv);
std::exit(EXIT_FAILURE);
}
}
if (optind < argc) {
usage(*argv);
std::exit(EXIT_FAILURE);
}
std::exit(validate::run(opts));
}
/// parse the benchmark command options
[[noreturn]] void on_benchmark(int argc, char** argv) {
benchmark::Options opts{};
const std::array<option, 10> GETOPT {{
{ "dll", required_argument, nullptr, 'd' },
{ "allow-fp16", no_argument, nullptr, 'a' },
{ "width", required_argument, nullptr, 'w' },
{ "height", required_argument, nullptr, 'h' },
{ "flow", required_argument, nullptr, 'f' },
{ "multiplier", required_argument, nullptr, 'm' },
{ "performance-mode", no_argument, nullptr, 'p' },
{ "gpu", required_argument, nullptr, 'g' },
{ "duration", required_argument, nullptr, 't' },
{ nullptr, no_argument, nullptr, 0 }
}};
int c{0};
while ((c = getopt_long(argc, argv, "d:aw:h:f:m:pg:t:", GETOPT.data(), nullptr)) != -1) {
switch (c) {
case 'd':
opts.dll.emplace(optarg);
break;
case 'a':
opts.allow_fp16 = true;
break;
case 'w':
opts.width = std::stoi(optarg);
break;
case 'h':
opts.height = std::stoi(optarg);
break;
case 'f':
opts.flow = std::stof(optarg);
break;
case 'm':
opts.multiplier = std::stoi(optarg);
break;
case 'p':
opts.performance_mode = true;
break;
case 'g':
opts.gpu.emplace(optarg);
break;
case 't':
opts.duration = std::stoi(optarg);
break;
case '?':
default:
usage(*argv);
std::exit(EXIT_FAILURE);
}
}
if (optind < argc) {
usage(*argv);
std::exit(EXIT_FAILURE);
}
std::exit(benchmark::run(opts));
}
/// parse the debug command options
[[noreturn]] void on_debug(int argc, char** argv) {
debug::Options opts{};
const std::array<option, 9> GETOPT {{
{ "dll", required_argument, nullptr, 'd' },
{ "allow-fp16", no_argument, nullptr, 'a' },
{ "width", required_argument, nullptr, 'w' },
{ "height", required_argument, nullptr, 'h' },
{ "flow", required_argument, nullptr, 'f' },
{ "multiplier", required_argument, nullptr, 'm' },
{ "performance-mode", no_argument, nullptr, 'p' },
{ "gpu", required_argument, nullptr, 'g' },
{ nullptr, no_argument, nullptr, 0 }
}};
int c{0};
while ((c = getopt_long(argc, argv, "d:aw:h:f:m:pg:", GETOPT.data(), nullptr)) != -1) {
switch (c) {
case 'd':
opts.dll.emplace(optarg);
break;
case 'a':
opts.allow_fp16 = true;
break;
case 'w':
opts.width = std::stoi(optarg);
break;
case 'h':
opts.height = std::stoi(optarg);
break;
case 'f':
opts.flow = std::stof(optarg);
break;
case 'm':
opts.multiplier = std::stoi(optarg);
break;
case 'p':
opts.performance_mode = true;
break;
case 'g':
opts.gpu.emplace(optarg);
break;
case '?':
default:
usage(*argv);
std::exit(EXIT_FAILURE);
}
}
if ((optind + 1) != argc) {
usage(*argv);
std::exit(EXIT_FAILURE);
}
opts.path = argv[optind];
std::exit(debug::run(opts));
}
}
int main(int argc, char** argv) {
if (argc < 2) {
usage(*argv);
return EXIT_FAILURE;
}
const std::string command{argv[1]};
if (command == "validate")
on_validate(argc - 1, argv + 1);
else if (command == "benchmark")
on_benchmark(argc - 1, argv + 1);
else if (command == "debug")
on_debug(argc - 1, argv + 1);
usage(*argv);
return EXIT_FAILURE;
}

View file

@ -0,0 +1,181 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "benchmark.hpp"
#include "lsfg-vk-backend/lsfgvk.hpp"
#include "lsfg-vk-common/helpers/errors.hpp"
#include "lsfg-vk-common/helpers/paths.hpp"
#include "lsfg-vk-common/vulkan/image.hpp"
#include "lsfg-vk-common/vulkan/timeline_semaphore.hpp"
#include "lsfg-vk-common/vulkan/vulkan.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <vulkan/vulkan_core.h>
using namespace lsfgvk::cli;
using namespace lsfgvk::cli::benchmark;
namespace {
// get current time in milliseconds
uint64_t ms() {
struct timespec ts{};
clock_gettime(CLOCK_MONOTONIC, &ts); // NOLINT (IWYU)
return static_cast<uint64_t>(ts.tv_sec) * 1000ULL +
static_cast<uint64_t>(ts.tv_nsec) / 1000000ULL;
}
}
int benchmark::run(const Options& opts) {
try {
// parse options
if (opts.flow < 0.25F || opts.flow > 1.0F)
throw ls::error("flow scale must be between 0.25 and 1.0");
if (opts.multiplier < 2)
throw ls::error("multiplier must be 2 or greater");
if (opts.width <= 0 || opts.height <= 0)
throw ls::error("width and height must be positive integers");
if (opts.duration <= 0)
throw ls::error("duration must be a positive integer");
const VkExtent2D extent{
static_cast<uint32_t>(opts.width),
static_cast<uint32_t>(opts.height)
};
// create instance
const vk::Vulkan vk{
"lsfg-vk-debug", vk::version{2, 0, 0},
"lsfg-vk-debug-engine", vk::version{2, 0, 0},
[opts](const vk::VulkanInstanceFuncs fi,
const std::vector<VkPhysicalDevice>& devices) {
if (!opts.gpu.has_value())
return devices.front();
for (const VkPhysicalDevice& device : devices) {
VkPhysicalDeviceProperties2 props{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2
};
fi.GetPhysicalDeviceProperties2(device, &props);
auto& properties = props.properties;
std::array<char, 256> devname = std::to_array(properties.deviceName);
devname[255] = '\0'; // ensure null-termination
if (std::string(devname.data()) == *opts.gpu)
return device;
}
throw ls::error("failed to find specified GPU: " + *opts.gpu);
}
};
std::pair<int, int> srcfds{};
const vk::Image frame_0{vk,
extent, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
std::nullopt, &srcfds.first};
const vk::Image frame_1{vk,
extent, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
std::nullopt, &srcfds.second};
std::vector<vk::Image> destimgs{};
std::vector<int> destfds{};
for (int i = 0; i < (opts.multiplier - 1); i++) {
int fd{};
destimgs.emplace_back(vk,
extent, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
std::nullopt,
&fd
);
destfds.push_back(fd);
}
int syncfd{};
const vk::TimelineSemaphore sync{vk, 0, std::nullopt, &syncfd};
// initialize backend
std::string dll{};
if (opts.dll.has_value())
dll = *opts.dll;
else
dll = ls::findShaderDll();
lsfgvk::backend::Instance lsfgvk{
[opts](
const std::string& gpu_name,
std::pair<const std::string&, const std::string&>,
const std::optional<std::string>&
) {
return opts.gpu.value_or(gpu_name) == gpu_name;
},
dll, opts.allow_fp16
};
lsfgvk::backend::Context& lsfgvk_ctx = lsfgvk.openContext(
srcfds, destfds,
syncfd, extent.width, extent.height,
false, 1.0F / opts.flow, opts.performance_mode
);
// run the benchmark
size_t iterations{0};
size_t generated_frames{0};
size_t total_frames{1};
uint64_t print_time = ms() + 1000ULL;
const uint64_t end_time = ms() + static_cast<uint64_t>(opts.duration) * 1000ULL;
while (ms() < end_time) {
sync.signal(vk, total_frames++);
lsfgvk.scheduleFrames(lsfgvk_ctx);
for (size_t i = 0; i < destimgs.size(); i++) {
auto success = sync.wait(vk, total_frames++);
if (!success)
throw ls::error("failed to wait for frame");
generated_frames++;
}
iterations++;
if (ms() >= print_time) {
print_time += 1000ULL;
std::cerr << "." << std::flush;
}
}
// output results
std::cerr << (opts.duration < 40 ? "\r" : "\n");
std::cerr << "benchmark results (ran for " << opts.duration << " seconds):\n";
std::cerr << " iterations: " << iterations << "\n";
std::cerr << " generated frames: " << generated_frames << "\n";
std::cerr << " total frames: " << total_frames << "\n";
const auto time = static_cast<double>(opts.duration);
const double fps_generated = static_cast<double>(generated_frames) / time;
const double fps_total = static_cast<double>(total_frames) / time;
std::cerr << std::setprecision(2) << std::fixed;
std::cerr << " fps (generated): " << fps_generated << "fps\n";
std::cerr << " fps (total): " << fps_total << "fps\n";
// deinitialize lsfg-vk
lsfgvk.closeContext(lsfgvk_ctx);
return EXIT_SUCCESS;
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << "\n";
return EXIT_FAILURE;
}
}

View file

@ -0,0 +1,29 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#pragma once
#include <optional>
#include <string>
namespace lsfgvk::cli::benchmark {
/// options for the "benchmark" command
struct Options {
std::optional<std::string> dll;
bool allow_fp16{true};
int width{1920};
int height{1080};
float flow{0.85F};
int multiplier{2};
bool performance_mode{true};
std::optional<std::string> gpu;
int duration{10};
};
/// run the "benchmark" command
/// @param opts the command options
int run(const Options& opts);
}

View file

@ -0,0 +1,198 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "debug.hpp"
#include "lsfg-vk-backend/lsfgvk.hpp"
#include "lsfg-vk-common/helpers/errors.hpp"
#include "lsfg-vk-common/helpers/paths.hpp"
#include "lsfg-vk-common/vulkan/buffer.hpp"
#include "lsfg-vk-common/vulkan/command_buffer.hpp"
#include "lsfg-vk-common/vulkan/image.hpp"
#include "lsfg-vk-common/vulkan/timeline_semaphore.hpp"
#include "lsfg-vk-common/vulkan/vulkan.hpp"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <vulkan/vulkan_core.h>
using namespace lsfgvk::cli;
using namespace lsfgvk::cli::debug;
namespace {
/// uploads an image from a dds file
void upload_image(const vk::Vulkan& vk,
const vk::Image& image, const std::string& path) {
// read image bytecode
std::ifstream file(path.data(), std::ios::binary | std::ios::ate);
if (!file.is_open())
throw ls::error("ifstream::ifstream() failed");
std::streamsize size = file.tellg();
size -= 124 + 4; // dds header and magic bytes
std::vector<char> code(static_cast<size_t>(size));
file.seekg(124 + 4, std::ios::beg);
if (!file.read(code.data(), size))
throw ls::error("ifstream::read() failed");
file.close();
// upload to image
const vk::Buffer stagingbuf{vk, code.data(), code.size(),
VK_BUFFER_USAGE_TRANSFER_SRC_BIT};
const vk::CommandBuffer cmdbuf{vk};
cmdbuf.begin(vk);
cmdbuf.copyBufferToImage(vk, stagingbuf, image);
cmdbuf.end(vk);
const vk::TimelineSemaphore sema{vk, 0};
cmdbuf.submit(vk);
}
}
int debug::run(const Options& opts) {
try {
// parse options
if (opts.flow < 0.25F || opts.flow > 1.0F)
throw ls::error("flow scale must be between 0.25 and 1.0");
if (opts.multiplier < 2)
throw ls::error("multiplier must be 2 or greater");
if (opts.width <= 0 || opts.height <= 0)
throw ls::error("width and height must be positive integers");
const VkExtent2D extent{
static_cast<uint32_t>(opts.width),
static_cast<uint32_t>(opts.height)
};
if (!std::filesystem::exists(opts.path))
throw ls::error("debug path does not exist: " + opts.path.string());
std::vector<std::filesystem::path> paths{};
for (const auto& entry : std::filesystem::directory_iterator(opts.path))
paths.push_back(entry.path());
std::ranges::sort(paths, [](const std::filesystem::path& a, const std::filesystem::path& b) {
auto fa = a.filename().string();
auto fb = b.filename().string();
auto norm_a = fa.find_first_of('.');
if (norm_a == std::string::npos)
throw ls::error("invalid debug file name: " + fa);
auto norm_b = fb.find_first_of('.');
if (norm_b == std::string::npos)
throw ls::error("invalid debug file name: " + fb);
return std::stoi(fa.substr(0, norm_a)) < std::stoi(fb.substr(0, norm_b));
});
// create instance
const vk::Vulkan vk{
"lsfg-vk-debug", vk::version{2, 0, 0},
"lsfg-vk-debug-engine", vk::version{2, 0, 0},
[opts](const vk::VulkanInstanceFuncs fi,
const std::vector<VkPhysicalDevice>& devices) {
if (!opts.gpu.has_value())
return devices.front();
for (const VkPhysicalDevice& device : devices) {
VkPhysicalDeviceProperties2 props{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2
};
fi.GetPhysicalDeviceProperties2(device, &props);
auto& properties = props.properties;
std::array<char, 256> devname = std::to_array(properties.deviceName);
devname[255] = '\0'; // ensure null-termination
if (std::string(devname.data()) == *opts.gpu)
return device;
}
throw ls::error("failed to find specified GPU: " + *opts.gpu);
}
};
std::pair<int, int> srcfds{};
const vk::Image frame_0{vk,
extent, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
std::nullopt, &srcfds.first};
const vk::Image frame_1{vk,
extent, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
std::nullopt, &srcfds.second};
std::vector<vk::Image> destimgs{};
std::vector<int> destfds{};
for (int i = 0; i < (opts.multiplier - 1); i++) {
int fd{};
destimgs.emplace_back(vk,
extent, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
std::nullopt,
&fd
);
destfds.push_back(fd);
}
int syncfd{};
const vk::TimelineSemaphore sync{vk, 0, std::nullopt, &syncfd};
// initialize backend
std::string dll{};
if (opts.dll.has_value())
dll = *opts.dll;
else
dll = ls::findShaderDll();
lsfgvk::backend::Instance lsfgvk{
[opts](
const std::string& gpu_name,
std::pair<const std::string&, const std::string&>,
const std::optional<std::string>&
) {
return opts.gpu.value_or(gpu_name) == gpu_name;
},
dll, opts.allow_fp16
};
lsfgvk::backend::Context& lsfgvk_ctx = lsfgvk.openContext(
srcfds, destfds,
syncfd, extent.width, extent.height,
false, 1.0F / opts.flow, opts.performance_mode
);
// render destination images
size_t idx{1};
for (size_t j = 0; j < paths.size(); j++) {
upload_image(vk,
j % 2 == 0 ? frame_0 : frame_1,
paths.at(j).string()
);
sync.signal(vk, idx++);
lsfgvk.scheduleFrames(lsfgvk_ctx);
for (size_t i = 0; i < destimgs.size(); i++) {
auto success = sync.wait(vk, idx++);
if (!success)
throw ls::error("failed to wait for frame");
}
}
// deinitialize lsfg-vk
lsfgvk.closeContext(lsfgvk_ctx);
return EXIT_SUCCESS;
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << "\n";
return EXIT_FAILURE;
}
}

View file

@ -0,0 +1,30 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#pragma once
#include <filesystem>
#include <optional>
#include <string>
namespace lsfgvk::cli::debug {
/// options for the "debug" command
struct Options {
std::optional<std::string> dll;
bool allow_fp16{true};
int width{1920};
int height{1080};
float flow{0.85F};
int multiplier{2};
bool performance_mode{true};
std::optional<std::string> gpu;
std::filesystem::path path;
};
/// run the "debug" command
/// @param opts the command options
int run(const Options& opts);
}

View file

@ -0,0 +1,31 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "validate.hpp"
#include "lsfg-vk-common/configuration/config.hpp"
#include <exception>
#include <filesystem>
#include <iostream>
using namespace lsfgvk::cli;
using namespace lsfgvk::cli::validate;
int validate::run(const Options& opts) {
std::filesystem::path path{ls::findConfigurationFile()};
if (opts.config.has_value())
path = *opts.config;
if (!std::filesystem::exists(path)) {
std::cerr << "Validation failed: configuration file does not exist\n";
return 1;
}
try {
const ls::ConfigFile config{path};
std::cerr << "Validation success\n";
} catch (const std::exception& e) {
std::cerr << "Validation failed: " << e.what() << '\n';
return 1;
}
return 0;
}

View file

@ -0,0 +1,19 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#pragma once
#include <optional>
#include <string>
namespace lsfgvk::cli::validate {
/// options for the "validate" command
struct Options {
std::optional<std::string> config;
};
/// run the "validate" command
/// @param opts the command options
int run(const Options& opts);
}

View file

@ -2,6 +2,7 @@ set(COMMON_SOURCES
"src/configuration/config.cpp"
"src/configuration/detection.cpp"
"src/helpers/errors.cpp"
"src/helpers/paths.cpp"
"src/vulkan/buffer.cpp"
"src/vulkan/command_buffer.cpp"
"src/vulkan/descriptor_pool.cpp"

View file

@ -0,0 +1,14 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#pragma once
#include <filesystem>
namespace ls {
/// find the location of the Lossless.dll
/// @returns the path to the DLL
/// @throws ls::error if the DLL could not be found
std::filesystem::path findShaderDll();
}

View file

@ -0,0 +1,49 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "lsfg-vk-common/helpers/paths.hpp"
#include "lsfg-vk-common/helpers/errors.hpp"
#include <cstdlib>
#include <filesystem>
#include <vector>
std::filesystem::path ls::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 ls::error("unable to locate Lossless.dll, please set the path in the configuration");
}

View file

@ -1,8 +0,0 @@
set(DEBUG_SOURCES
"src/debug.cpp")
add_executable(lsfg-vk-debug ${DEBUG_SOURCES})
target_link_libraries(lsfg-vk-debug
PUBLIC lsfg-vk-common
PUBLIC lsfg-vk-backend)

View file

@ -1,174 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "lsfg-vk-backend/lsfgvk.hpp"
#include "lsfg-vk-common/vulkan/buffer.hpp"
#include "lsfg-vk-common/vulkan/command_buffer.hpp"
#include "lsfg-vk-common/vulkan/image.hpp"
#include "lsfg-vk-common/vulkan/timeline_semaphore.hpp"
#include "lsfg-vk-common/vulkan/vulkan.hpp"
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <fstream>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include <bits/time.h>
#include <time.h> // NOLINT (thanks clang-tidy)
#include <vulkan/vulkan_core.h>
const VkExtent2D EXTENT = { 1920, 1080 };
namespace {
/// returns the current time in microseconds
uint64_t get_current_time_us() {
struct timespec ts{};
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<uint64_t>(ts.tv_sec) * 1000000UL
+ static_cast<uint64_t>(ts.tv_nsec) / 1000UL;
}
/// uploads an image from a dds file
void upload_image(
const vk::Vulkan& vk,
const vk::Image& image,
const std::string& path
) {
// read image bytecode
std::ifstream file(path.data(), std::ios::binary | std::ios::ate);
if (!file.is_open())
throw std::runtime_error("ifstream::ifstream() failed");
std::streamsize size = file.tellg();
size -= 124 + 4; // dds header and magic bytes
std::vector<char> code(static_cast<size_t>(size));
file.seekg(124 + 4, std::ios::beg);
if (!file.read(code.data(), size))
throw std::runtime_error("ifstream::read() failed");
file.close();
// upload to image
const vk::Buffer stagingbuf{vk, code.data(), code.size(),
VK_BUFFER_USAGE_TRANSFER_SRC_BIT};
const vk::CommandBuffer cmdbuf{vk};
cmdbuf.begin(vk);
cmdbuf.copyBufferToImage(vk, stagingbuf, image);
cmdbuf.end(vk);
const vk::TimelineSemaphore sema{vk, 0};
cmdbuf.submit(vk);
}
}
int main() {
const uint64_t time_us = get_current_time_us();
const vk::Vulkan vk{
"lsfg-vk-debug", vk::version{2, 0, 0},
"lsfg-vk-debug-engine", vk::version{2, 0, 0},
[](const vk::VulkanInstanceFuncs,
const std::vector<VkPhysicalDevice>& devices) {
return devices.front();
}
};
std::pair<int, int> srcfds{};
const vk::Image frame_0{vk,
EXTENT, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
std::nullopt,
&srcfds.first
};
const vk::Image frame_1{vk,
EXTENT, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
std::nullopt,
&srcfds.second
};
std::vector<vk::Image> destimgs{};
std::vector<int> destfds{};
for (size_t i = 0; i < 4; i++) {
int fd{};
destimgs.emplace_back(vk,
EXTENT, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
std::nullopt,
&fd
);
destfds.push_back(fd);
}
int syncfd{};
const vk::TimelineSemaphore sync{vk, 0, std::nullopt, &syncfd};
const uint64_t init_done_us = get_current_time_us();
std::cerr << "vulkan initialized in "
<< (init_done_us - time_us) << "us\n";
// initialize lsfg-vk
lsfgvk::backend::Instance lsfgvk{
[](
const std::string&,
std::pair<const std::string&, const std::string&>,
const std::optional<std::string>&
) {
return true;
},
"/home/pancake/.steam/steam/steamapps/common/Lossless Scaling/Lossless.dll",
true
};
lsfgvk::backend::Context& lsfgvk_ctx = lsfgvk.openContext(
srcfds, destfds,
syncfd, EXTENT.width, EXTENT.height,
false, 1.0F / 0.5F, true
);
const uint64_t lsfg_init_done_us = get_current_time_us();
std::cerr << "lsfg-vk initialized in "
<< (lsfg_init_done_us - init_done_us) << "us\n";
// render destination images
size_t idx{1};
for (size_t j = 0; j < 3; j++) {
try {
upload_image(vk,
j % 2 == 0 ? frame_0 : frame_1,
"s" + std::to_string(j + 1) + ".dds"
);
} catch (const std::exception& e) {
std::cerr << "failed to upload image: " << e.what() << "\n";
return EXIT_FAILURE;
}
sync.signal(vk, idx++);
lsfgvk.scheduleFrames(lsfgvk_ctx);
for (size_t i = 0; i < destimgs.size(); i++) {
auto success = sync.wait(vk, idx++);
if (!success) {
std::cerr << "failed to wait for frame " << j << ":" << i << "\n";
return EXIT_FAILURE;
}
const uint64_t frame_done_us = get_current_time_us();
std::cerr << "frame " << j << ":" << i << " done after "
<< (frame_done_us - lsfg_init_done_us) << "us\n";
}
}
// deinitialize lsfg-vk
lsfgvk.closeContext(lsfgvk_ctx);
return EXIT_SUCCESS; // let the vulkan objects go out of scope
}

View file

@ -1,6 +1,7 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "instance.hpp"
#include "lsfg-vk-common/helpers/paths.hpp"
#include "swapchain.hpp"
#include "lsfg-vk-common/configuration/detection.hpp"
#include "lsfg-vk-common/helpers/errors.hpp"
@ -10,7 +11,6 @@
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <filesystem>
#include <functional>
#include <iostream>
#include <optional>
@ -41,47 +41,6 @@ namespace {
return extensions;
}
// find the shader dll
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 ls::error("unable to locate Lossless.dll, please set the path in the configuration");
}
}
Root::Root() {
@ -216,6 +175,12 @@ void Root::createSwapchainContext(const vk::Vulkan& vk,
setenv("DISABLE_LSFGVK", "1", 1); // NOLINT (c++-include)
try {
std::string dll{};
if (global.dll.has_value())
dll = *global.dll;
else
dll = ls::findShaderDll();
this->backend.emplace(
[gpu = profile.gpu](
const std::string& deviceName,
@ -229,8 +194,7 @@ void Root::createSwapchainContext(const vk::Vulkan& vk,
|| (ids.first + ":" + ids.second == *gpu)
|| (pci && *pci == *gpu);
},
global.dll.value_or(findShaderDll()),
global.allow_fp16
dll, global.allow_fp16
);
} catch (const std::exception& e) {
unsetenv("DISABLE_LSFGVK"); // NOLINT (c++-include)

View file

@ -1,95 +0,0 @@
#include "utils/benchmark.hpp"
#include "config/config.hpp"
#include "extract/extract.hpp"
#include <vulkan/vulkan_core.h>
#include <lsfg_3_1.hpp>
#include <lsfg_3_1p.hpp>
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <cstdint>
#include <iomanip>
#include <thread>
#include <chrono>
#include <string>
#include <vector>
using namespace Benchmark;
void Benchmark::run(uint32_t width, uint32_t height) {
if (!Config::currentConf.has_value())
_exit(1); // not possible
const auto& globalConf = Config::globalConf;
const auto& conf = *Config::currentConf;
auto* lsfgInitialize = LSFG_3_1::initialize;
auto* lsfgCreateContext = LSFG_3_1::createContext;
auto* lsfgPresentContext = LSFG_3_1::presentContext;
if (conf.performance) {
lsfgInitialize = LSFG_3_1P::initialize;
lsfgCreateContext = LSFG_3_1P::createContext;
lsfgPresentContext = LSFG_3_1P::presentContext;
}
// create the benchmark context
const char* lsfgDeviceUUID = std::getenv("LSFG_DEVICE_UUID");
const uint64_t deviceUUID = lsfgDeviceUUID
? std::stoull(std::string(lsfgDeviceUUID), nullptr, 16) : 0x1463ABAC;
setenv("DISABLE_LSFG", "1", 1); // NOLINT
Extract::extractShaders();
lsfgInitialize(
deviceUUID, // some magic number if not given
conf.hdr, 1.0F / conf.flowScale, conf.multiplier - 1,
globalConf.no_fp16,
Extract::getShader
);
const int32_t ctx = lsfgCreateContext(-1, -1, {},
{ .width = width, .height = height },
conf.hdr ? VK_FORMAT_R16G16B16A16_SFLOAT : VK_FORMAT_R8G8B8A8_UNORM
);
unsetenv("DISABLE_LSFG"); // NOLINT
// run the benchmark (run 8*n + 1 so the fences are waited on)
const auto now = std::chrono::high_resolution_clock::now();
const uint64_t iterations = 8 * 500UL;
std::cerr << "lsfg-vk: Benchmark started, running " << iterations << " iterations...\n";
for (uint64_t count = 0; count < iterations + 1; count++) {
lsfgPresentContext(ctx, -1, {});
if (count % 50 == 0 && count > 0)
std::cerr << "lsfg-vk: "
<< std::setprecision(2) << std::fixed
<< static_cast<float>(count) / static_cast<float>(iterations) * 100.0F
<< "% done (" << count + 1 << "/" << iterations << ")\r";
}
const auto then = std::chrono::high_resolution_clock::now();
// print results
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(then - now).count();
const auto perIteration = static_cast<float>(ms) / static_cast<float>(iterations);
const uint64_t totalGen = (conf.multiplier - 1) * iterations;
const auto genFps = static_cast<float>(totalGen) / (static_cast<float>(ms) / 1000.0F);
const uint64_t totalFrames = iterations * conf.multiplier;
const auto totalFps = static_cast<float>(totalFrames) / (static_cast<float>(ms) / 1000.0F);
std::cerr << "lsfg-vk: Benchmark completed in " << ms << " ms\n";
std::cerr << " Time taken per real frame: "
<< std::setprecision(2) << std::fixed << perIteration << " ms\n";
std::cerr << " Generated " << totalGen << " frames in total at "
<< std::setprecision(2) << std::fixed << genFps << " FPS\n";
std::cerr << " Total of " << totalFrames << " frames presented at "
<< std::setprecision(2) << std::fixed << totalFps << " FPS\n";
// sleep for a second, then exit
std::this_thread::sleep_for(std::chrono::seconds(1));
_exit(0);
}