diff --git a/lsfg-vk-ui/CMakeLists.txt b/lsfg-vk-ui/CMakeLists.txt index cb57b54..c566051 100644 --- a/lsfg-vk-ui/CMakeLists.txt +++ b/lsfg-vk-ui/CMakeLists.txt @@ -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) diff --git a/lsfg-vk-ui/rsc/UI.qml b/lsfg-vk-ui/rsc/UI.qml index a9f0430..529c905 100644 --- a/lsfg-vk-ui/rsc/UI.qml +++ b/lsfg-vk-ui/rsc/UI.qml @@ -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] + } } } diff --git a/lsfg-vk-ui/rsc/panes/ProfileList.qml b/lsfg-vk-ui/rsc/panes/ProfileList.qml index 57caa33..a7802c9 100644 --- a/lsfg-vk-ui/rsc/panes/ProfileList.qml +++ b/lsfg-vk-ui/rsc/panes/ProfileList.qml @@ -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) + } + } } } diff --git a/lsfg-vk-ui/rsc/widgets/FileEdit.qml b/lsfg-vk-ui/rsc/widgets/FileEdit.qml index eb020f4..bedf7bd 100644 --- a/lsfg-vk-ui/rsc/widgets/FileEdit.qml +++ b/lsfg-vk-ui/rsc/widgets/FileEdit.qml @@ -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://", "")) } } diff --git a/lsfg-vk-ui/rsc/widgets/FlowSlider.qml b/lsfg-vk-ui/rsc/widgets/FlowSlider.qml index b314b4c..ef0a276 100644 --- a/lsfg-vk-ui/rsc/widgets/FlowSlider.qml +++ b/lsfg-vk-ui/rsc/widgets/FlowSlider.qml @@ -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 } } diff --git a/lsfg-vk-ui/src/backend.cpp b/lsfg-vk-ui/src/backend.cpp index 036c579..711460a 100644 --- a/lsfg-vk-ui/src/backend.cpp +++ b/lsfg-vk-ui/src/backend.cpp @@ -1,9 +1,43 @@ #include "backend.hpp" +#include "lsfg-vk-common/configuration/config.hpp" -#include +#include +#include +#include +#include +#include +#include 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(); } diff --git a/lsfg-vk-ui/src/backend.hpp b/lsfg-vk-ui/src/backend.hpp index 19abf41..1bc324b 100644 --- a/lsfg-vk-ui/src/backend.hpp +++ b/lsfg-vk-ui/src/backend.hpp @@ -1,16 +1,158 @@ #pragma once +#include "lsfg-vk-common/configuration/config.hpp" + #include +#include +#include +#include +#include + +#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(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(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 m_profiles; + + QStringListModel* m_profile_list_model; + int m_profile_index{-1}; + + std::atomic_bool m_dirty{false}; }; } diff --git a/lsfg-vk-ui/src/main.cpp b/lsfg-vk-ui/src/main.cpp index dabe473..9b921ff 100644 --- a/lsfg-vk-ui/src/main.cpp +++ b/lsfg-vk-ui/src/main.cpp @@ -1,4 +1,5 @@ -#include "ui.hpp" +#include "backend.hpp" + #include #include #include @@ -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(); diff --git a/lsfg-vk-ui/src/ui.cpp b/lsfg-vk-ui/src/ui.cpp deleted file mode 100644 index 100a642..0000000 --- a/lsfg-vk-ui/src/ui.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "ui.hpp" - -#include -#include - -using namespace lsfgvk::ui; - -UI::UI(QObject* parent) : QObject(parent) { - std::cerr << "Hello, world!" << '\n'; -} diff --git a/lsfg-vk-ui/src/ui.hpp b/lsfg-vk-ui/src/ui.hpp deleted file mode 100644 index b900d1b..0000000 --- a/lsfg-vk-ui/src/ui.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -namespace lsfgvk::ui { - - class UI : public QObject { - Q_OBJECT - - public: - explicit UI(QObject* parent = nullptr); - private: - - }; - -}