refactor(cleanup): synchronize configuration state with ui

This commit is contained in:
PancakeTAS 2025-12-23 05:49:35 +01:00
parent 2f4b0cc5db
commit effd469b5e
10 changed files with 358 additions and 49 deletions

View file

@ -1,7 +1,7 @@
find_package(Qt6 REQUIRED COMPONENTS Quick)
set(UI_SOURCES
"src/ui.cpp"
"src/backend.cpp"
"src/main.cpp")
set(UI_RESOURCES
@ -28,5 +28,6 @@ target_compile_options(lsfg-vk-ui PRIVATE
-Wno-unsafe-buffer-usage-in-libc-call
-Wno-global-constructors)
target_link_libraries(lsfg-vk-ui PRIVATE
Qt6::Quick)
target_link_libraries(lsfg-vk-ui
PUBLIC lsfg-vk-common
PRIVATE Qt6::Quick)

View file

@ -13,6 +13,51 @@ ApplicationWindow {
minimumHeight: 400
visible: true
Dialog {
id: dialog_name
title: "(...)"
standardButtons: Dialog.Ok | Dialog.Cancel
modal: true
dim: true
x: (parent.width - width) / 2
y: (parent.height - height) / 2
contentItem: ColumnLayout {
spacing: 8
TextField {
Layout.fillWidth: true
id: nameField
placeholderText: "Choose a profile name"
selectByMouse: true
focus: true
}
}
}
Dialog {
id: dialog_confirm
title: "Confirm Deletion"
standardButtons: Dialog.Ok | Dialog.Cancel
modal: true
dim: true
x: (parent.width - width) / 2
y: (parent.height - height) / 2
contentItem: ColumnLayout {
spacing: 8
Label {
Layout.fillWidth: true
text: "Are you sure you want to delete the selected profile?"
horizontalAlignment: Text.AlignHCenter
}
}
}
SplitView {
anchors.fill: parent
orientation: Qt.Horizontal
@ -30,20 +75,37 @@ ApplicationWindow {
}
ProfileList {
model: backend.profiles
selected: backend.profile_index
onSelect: (index) => backend.profile_index = index
}
Button {
Layout.fillWidth: true
text: "Create New Profile"
Layout.fillWidth: true
onClicked: {
dialog_name.title = "Create New Profile"
nameField.text = ""
dialog_name.open()
}
}
Button {
Layout.fillWidth: true
text: "Rename Profile"
Layout.fillWidth: true
onClicked: {
dialog_name.title = "Rename Profile"
nameField.text = "(...)"
dialog_name.open()
}
}
Button {
text: "Delete Profile"
Layout.fillWidth: true
text: "Delete Profile"
onClicked: {
dialog_confirm.open()
}
}
}
@ -54,56 +116,91 @@ ApplicationWindow {
name: "Global Settings"
GroupEntry {
title: "Browse for Lossless.dll"
title: "Path to Lossless Scaling"
description: "Change the location of Lossless.dll"
FileEdit {}
FileEdit {
title: "Select Lossless.dll"
filter: "Dynamic Link Library Files (*.dll)"
text: backend.dll
onUpdate: (text) => backend.dll = text
}
}
GroupEntry {
title: "Allow half-precision"
description: "Allow acceleration through half-precision"
CheckBox {}
CheckBox {
checked: backend.allow_fp16
onToggled: backend.allow_fp16 = checked
}
}
}
Group {
name: "Profile Settings"
enabled: backend.available
GroupEntry {
title: "Multiplier"
description: "Control the amount of generated frames"
SpinBox { from: 2; to: 100 }
SpinBox {
from: 2
to: 100
value: backend.multiplier
onValueModified: backend.multiplier = value
}
}
GroupEntry {
title: "Flow Scale"
description: "Lower the internal motion estimation resolution"
FlowSlider { from: 0.25; to: 1.00 }
FlowSlider {
from: 0.25
to: 1.00
value: backend.flow_scale
onUpdate: (value) => backend.flow_scale = value
}
}
GroupEntry {
title: "Performance Mode"
description: "Use a significantly lighter frame generation modeln"
CheckBox {}
CheckBox {
checked: backend.performance_mode
onToggled: backend.performance_mode = checked
}
}
GroupEntry {
title: "Pacing Mode"
description: "Change how frames are presented to the display"
ComboBox { model: ["None"] }
ComboBox {
model: ["None"]
currentValue: backend.pacing_mode
onActivated: (index) => backend.pacing_mode = model[index]
}
}
GroupEntry {
title: "GPU"
description: "Select which GPU to use for frame generation"
ComboBox { model: ["Auto"] }
ComboBox {
model: ["Default"]
currentValue: backend.gpu
onActivated: (index) => backend.gpu = model[index]
}
}
}

View file

@ -2,6 +2,10 @@ import QtQuick
import QtQuick.Layouts
Rectangle {
property var model
property int selected
signal select(int index)
Layout.fillWidth: true
Layout.fillHeight: true
id: root
@ -12,5 +16,31 @@ Rectangle {
ListView {
anchors.fill: parent
id: view
model: root.model
currentIndex: root.selected
delegate: Rectangle {
width: view.width
height: 32
color: ListView.isCurrentItem ? palette.highlight : "transparent"
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 10
text: display
color: ListView.isCurrentItem ? palette.highlightedText : palette.text
}
MouseArea {
anchors.fill: parent
onClicked: root.select(index)
}
}
}
}

View file

@ -1,17 +1,34 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
RowLayout {
property string title
property string filter
property string text
signal update(string text)
id: root
spacing: 4
TextField {
Layout.fillWidth: true;
Layout.maximumWidth: 450;
text: root.text
onEditingFinished: root.update(text)
}
Button {
icon.name: "folder-open"
onClicked: picker.open()
}
FileDialog {
id: picker
title: root.title
nameFilters: [root.filter, "All Files (*)"]
onAccepted: root.update(selectedFile.toString().replace("file://", ""))
}
}

View file

@ -5,17 +5,30 @@ import QtQuick.Layouts
RowLayout {
property real from
property real to
property real value
signal update(real value)
id: root
spacing: 4
Slider {
Layout.fillWidth: true;
Layout.maximumWidth: 450;
value: root.value
from: root.from
to: root.to
onMoved: root.update(value.toFixed(2))
onValueChanged: label.text = (value * 100).toFixed(0) + "%"
}
Label {
Layout.preferredWidth: 40;
id: label
text: "0%"
horizontalAlignment: Text.AlignHCenter
}
}

View file

@ -1,9 +1,43 @@
#include "backend.hpp"
#include "lsfg-vk-common/configuration/config.hpp"
#include <QObject>
#include <QStringListModel>
#include <QStringList>
#include <QString>
#include <chrono>
#include <iostream>
#include <thread>
using namespace lsfgvk::ui;
Backend::Backend(QObject* parent) : QObject(parent) {
Backend::Backend() {
// load configuration
ls::Configuration config{false};
config.reload();
this->m_global = config.getGlobalConf();
this->m_profiles = config.getProfiles();
// create profile list model
QStringList profiles; // NOLINT (IWYU)
for (const auto& profile : this->m_profiles)
profiles.append(QString::fromStdString(profile.name));
this->m_profile_list_model = new QStringListModel(profiles, this);
// try to select first profile
if (!this->m_profiles.empty())
this->m_profile_index = 0;
// spawn saving thread
std::thread([this]() {
while (true) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (!this->m_dirty.exchange(false))
continue;
std::cerr << "configuration updated >:3" << '\n';
}
}).detach();
}

View file

@ -1,16 +1,158 @@
#pragma once
#include "lsfg-vk-common/configuration/config.hpp"
#include <QObject>
#include <QStringListModel>
#include <QString>
#include <atomic>
#include <utility>
#define getters public
#define setters public
namespace lsfgvk::ui {
/// Class tying ui and configuration together
class Backend : public QObject {
Q_OBJECT
public:
explicit Backend(QObject* parent = nullptr);
private:
Q_PROPERTY(QStringListModel* profiles READ calculateProfileListModel NOTIFY refreshUI)
Q_PROPERTY(int profile_index READ getProfileIndex WRITE profileSelected NOTIFY refreshUI)
Q_PROPERTY(bool available READ isValidProfileIndex NOTIFY refreshUI)
Q_PROPERTY(QString dll READ getDll WRITE dllUpdated NOTIFY refreshUI)
Q_PROPERTY(bool allow_fp16 READ getAllowFP16 WRITE allowFP16Updated NOTIFY refreshUI)
Q_PROPERTY(size_t multiplier READ getMultiplier WRITE multiplierUpdated NOTIFY refreshUI)
Q_PROPERTY(float flow_scale READ getFlowScale WRITE flowScaleUpdated NOTIFY refreshUI)
Q_PROPERTY(bool performance_mode READ getPerformanceMode WRITE performanceModeUpdated NOTIFY refreshUI)
Q_PROPERTY(QString pacing_mode READ getPacingMode WRITE pacingModeUpdated NOTIFY refreshUI)
Q_PROPERTY(QString gpu READ getGPU WRITE gpuUpdated NOTIFY refreshUI)
public:
explicit Backend();
getters:
[[nodiscard]] QStringListModel* calculateProfileListModel() const {
return this->m_profile_list_model;
}
[[nodiscard]] bool isValidProfileIndex() const {
return this->m_profile_index >= 0 && std::cmp_less(this->m_profile_index, this->m_profiles.size());
}
[[nodiscard]] int getProfileIndex() const {
return this->m_profile_index;
}
[[nodiscard]] QString getDll() const {
return QString::fromStdString(this->m_global.dll.value_or(""));
}
[[nodiscard]] bool getAllowFP16() const {
return this->m_global.allow_fp16;
}
#define VALIDATE_AND_GET_PROFILE(default) \
if (!isValidProfileIndex()) return default; \
auto& conf = this->m_profiles[static_cast<size_t>(this->m_profile_index)];
[[nodiscard]] size_t getMultiplier() const {
VALIDATE_AND_GET_PROFILE(2)
return conf.multiplier;
}
[[nodiscard]] float getFlowScale() const {
VALIDATE_AND_GET_PROFILE(1.0F)
return conf.flow_scale;
}
[[nodiscard]] bool getPerformanceMode() const {
VALIDATE_AND_GET_PROFILE(false)
return conf.performance_mode;
}
[[nodiscard]] QString getPacingMode() const {
VALIDATE_AND_GET_PROFILE("None")
switch (conf.pacing) {
case ls::Pacing::None: return "None";
}
throw std::runtime_error("Unknown pacing type in backend");
}
[[nodiscard]] QString getGPU() const {
VALIDATE_AND_GET_PROFILE("Default")
return QString::fromStdString(conf.gpu.value_or("Default"));
}
#undef VALIDATE_AND_GET_PROFILE
setters:
void profileSelected(int idx) {
this->m_profile_index = idx;
emit refreshUI();
}
#define MARK_DIRTY() \
this->m_dirty.store(true, std::memory_order_relaxed); \
emit refreshUI();
void dllUpdated(const QString& dll) {
auto& conf = this->m_global;
if (dll.trimmed().isEmpty())
conf.dll = std::nullopt;
else
conf.dll = dll.toStdString();
MARK_DIRTY()
}
void allowFP16Updated(bool allow_fp16) {
auto& conf = this->m_global;
conf.allow_fp16 = allow_fp16;
MARK_DIRTY()
}
#define VALIDATE_AND_GET_PROFILE() \
if (!isValidProfileIndex()) return; \
auto& conf = this->m_profiles[static_cast<size_t>(this->m_profile_index)];
void multiplierUpdated(size_t multiplier) {
VALIDATE_AND_GET_PROFILE()
conf.multiplier = multiplier;
MARK_DIRTY()
}
void flowScaleUpdated(float flow_scale) {
VALIDATE_AND_GET_PROFILE()
conf.flow_scale = flow_scale;
MARK_DIRTY()
}
void performanceModeUpdated(bool performance_mode) {
VALIDATE_AND_GET_PROFILE()
conf.performance_mode = performance_mode;
MARK_DIRTY()
}
void pacingModeUpdated(const QString& pacing_mode) {
VALIDATE_AND_GET_PROFILE()
if (pacing_mode == "None")
conf.pacing = ls::Pacing::None;
MARK_DIRTY()
}
void gpuUpdated(const QString& gpu) {
VALIDATE_AND_GET_PROFILE()
if (gpu.trimmed().isEmpty())
conf.gpu = std::nullopt;
else
conf.gpu.emplace(gpu.toStdString());
MARK_DIRTY()
}
#undef VALIDATE_AND_GET_PROFILE
#undef MARK_DIRTY
signals:
void refreshUI();
private:
ls::GlobalConf m_global;
std::vector<ls::GameConf> m_profiles;
QStringListModel* m_profile_list_model;
int m_profile_index{-1};
std::atomic_bool m_dirty{false};
};
}

View file

@ -1,4 +1,5 @@
#include "ui.hpp"
#include "backend.hpp"
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
@ -12,9 +13,9 @@ int main(int argc, char* argv[]) {
QGuiApplication::setApplicationDisplayName("lsfg-vk-ui");
QQmlApplicationEngine engine;
UI ui;
Backend backend;
engine.rootContext()->setContextProperty("ui", &ui);
engine.rootContext()->setContextProperty("backend", &backend);
engine.load("qrc:/rsc/UI.qml");
return QGuiApplication::exec();

View file

@ -1,10 +0,0 @@
#include "ui.hpp"
#include <QObject>
#include <iostream>
using namespace lsfgvk::ui;
UI::UI(QObject* parent) : QObject(parent) {
std::cerr << "Hello, world!" << '\n';
}

View file

@ -1,16 +0,0 @@
#pragma once
#include <QObject>
namespace lsfgvk::ui {
class UI : public QObject {
Q_OBJECT
public:
explicit UI(QObject* parent = nullptr);
private:
};
}