ACMX 2.139.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
custom-uniforms.cpp
Go to the documentation of this file.
1#include "custom-uniforms.hpp"
3
4#include <QAbstractButton>
5#include <QClipboard>
6#include <QDialogButtonBox>
7#include <QDoubleSpinBox>
8#include <QGridLayout>
9#include <QGuiApplication>
10#include <QHBoxLayout>
11#include <QLabel>
12#include <QLineEdit>
13#include <QMessageBox>
14#include <QPushButton>
15#include <QRegularExpression>
16#include <QScrollArea>
17#include <QSignalBlocker>
18#include <QSlider>
19#include <QTimer>
20#include <QVBoxLayout>
21#include <algorithm>
22#include <cmath>
23
24namespace {
25 constexpr int MAX_SLIDER_STEPS = 10000000;
26
27 void configureDoubleSpinBox(QDoubleSpinBox *spin) {
28 spin->setDecimals(8);
29 spin->setRange(-1000000000.0, 1000000000.0);
30 spin->setKeyboardTracking(false);
31 spin->setMinimumWidth(130);
32 }
33
34 void disableDefaultButton(QPushButton *button) {
35 button->setAutoDefault(false);
36 button->setDefault(false);
37 }
38
40 const double count = std::ceil((uniform.maximum - uniform.minimum) / uniform.step);
41 if (count >= MAX_SLIDER_STEPS)
42 return MAX_SLIDER_STEPS;
43 return std::max(static_cast<int>(count), 1);
44 }
45
46 double sliderValue(const acmx2::CustomUniformDefinition &uniform, int position) { return std::min(uniform.maximum, uniform.minimum + uniform.step * position); }
47
48 int sliderPosition(const acmx2::CustomUniformDefinition &uniform, double value) {
49 const double position = std::round((value - uniform.minimum) / uniform.step);
50 return std::clamp(static_cast<int>(position), 0, sliderStepCount(uniform));
51 }
52
53 int decimalsForStep(double step) {
54 if (step >= 1.0)
55 return 6;
56 return std::clamp(static_cast<int>(std::ceil(-std::log10(step))) + 2, 2, 10);
57 }
58
60 if (uniform.slot < 0)
61 return {};
62 static const QString components = QStringLiteral("xyzw");
63 return QStringLiteral("ext.custom_uniforms[%1].%2").arg(uniform.slot / 4).arg(components.at(uniform.slot % 4));
64 }
65
67 if (backend == acmx2::Backend::Acmxvk) {
68 return QStringLiteral("#define %1 %2").arg(uniform.name, uniformLocation(uniform));
69 }
70 return QStringLiteral("uniform float %1;").arg(uniform.name);
71 }
72} // namespace
73
74CustomUniformDialog::CustomUniformDialog(QWidget *parent) : QDialog(parent) {
75 setWindowTitle(tr("Custom Uniforms"));
76 resize(850, 520);
77 setModal(false);
78
79 auto *mainLayout = new QVBoxLayout(this);
80 auto *addLayout = new QHBoxLayout();
81
82 nameEdit = new QLineEdit(this);
83 nameEdit->setPlaceholderText(tr("GLSL uniform name"));
84 minimumSpin = new QDoubleSpinBox(this);
85 maximumSpin = new QDoubleSpinBox(this);
86 stepSpin = new QDoubleSpinBox(this);
87 configureDoubleSpinBox(minimumSpin);
88 configureDoubleSpinBox(maximumSpin);
89 configureDoubleSpinBox(stepSpin);
90 minimumSpin->setValue(0.0);
91 maximumSpin->setValue(1.0);
92 stepSpin->setMinimum(0.00000001);
93 stepSpin->setValue(0.01);
94
95 addLayout->addWidget(new QLabel(tr("Name"), this));
96 addLayout->addWidget(nameEdit, 2);
97 addLayout->addWidget(new QLabel(tr("Minimum"), this));
98 addLayout->addWidget(minimumSpin);
99 addLayout->addWidget(new QLabel(tr("Maximum"), this));
100 addLayout->addWidget(maximumSpin);
101 addLayout->addWidget(new QLabel(tr("Step"), this));
102 addLayout->addWidget(stepSpin);
103 auto *addButton = new QPushButton(tr("Add Slider"), this);
104 disableDefaultButton(addButton);
105 addLayout->addWidget(addButton);
106 mainLayout->addLayout(addLayout);
107
108 auto *hint = new QLabel(tr("Names become GLSL float uniforms. Values and slots are saved in "
109 "library.json and sent live to the active engine."),
110 this);
111 hint->setWordWrap(true);
112 mainLayout->addWidget(hint);
113
114 scrollArea = new QScrollArea(this);
115 scrollArea->setWidgetResizable(true);
116 rowsWidget = new QWidget(scrollArea);
117 rowsLayout = new QVBoxLayout(rowsWidget);
118 rowsLayout->setAlignment(Qt::AlignTop);
119 scrollArea->setWidget(rowsWidget);
120 mainLayout->addWidget(scrollArea, 1);
121
122 auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
123 for (QAbstractButton *button : buttons->buttons()) {
124 if (auto *pushButton = qobject_cast<QPushButton *>(button))
125 disableDefaultButton(pushButton);
126 }
127 mainLayout->addWidget(buttons);
128
129 saveTimer = new QTimer(this);
130 saveTimer->setSingleShot(true);
131 saveTimer->setInterval(150);
132 connect(saveTimer, &QTimer::timeout, this, &CustomUniformDialog::savePendingChanges);
133 connect(addButton, &QPushButton::clicked, this, &CustomUniformDialog::addUniform);
134 connect(nameEdit, &QLineEdit::returnPressed, this, &CustomUniformDialog::addUniform);
135 connect(buttons, &QDialogButtonBox::rejected, this, &CustomUniformDialog::hide);
136
138}
139
140bool CustomUniformDialog::loadLibrary(const QString &directory, acmx2::Backend backend, QString *error) {
141 if (saveTimer->isActive()) {
142 saveTimer->stop();
143 saveUniforms(false);
144 }
145
146 activeBackend = backend;
147 QList<acmx2::CustomUniformDefinition> loadedUniforms;
148 QString loadError;
149 const auto clearFailedLoad = [this, &directory]() {
150 libraryDirectory = directory;
151 uniformDefinitions.clear();
153 emit uniformsChanged();
154 };
155 if (!acmx2::load_custom_uniforms(directory, loadedUniforms, loadError)) {
156 clearFailedLoad();
157 if (error)
158 *error = loadError;
159 return false;
160 }
161 if (loadedUniforms.size() > static_cast<int>(acmx2::ipc::kShaderSelectionMaxCustomUniforms)) {
162 clearFailedLoad();
163 if (error) {
164 *error = tr("library.json contains more than %1 custom uniforms.").arg(acmx2::ipc::kShaderSelectionMaxCustomUniforms);
165 }
166 return false;
167 }
168 for (const auto &uniform : loadedUniforms) {
169 const double stepCount = std::ceil((uniform.maximum - uniform.minimum) / uniform.step);
170 if (stepCount > MAX_SLIDER_STEPS) {
171 clearFailedLoad();
172 if (error) {
173 *error = tr("Custom uniform '%1' has more than %2 slider positions. Increase its step size.").arg(uniform.name).arg(MAX_SLIDER_STEPS);
174 }
175 return false;
176 }
177 }
178
179 bool addedExplicitSlots = false;
180 for (int index = 0; index < loadedUniforms.size(); ++index) {
181 if (loadedUniforms[index].slot < 0) {
182 loadedUniforms[index].slot = index;
183 addedExplicitSlots = true;
184 }
185 }
186 if (addedExplicitSlots && !acmx2::write_custom_uniforms(directory, loadedUniforms, loadError)) {
187 clearFailedLoad();
188 if (error)
189 *error = loadError;
190 return false;
191 }
192
193 libraryDirectory = directory;
194 uniformDefinitions = loadedUniforms;
196 emit uniformsChanged();
197 return true;
198}
199
200const QList<acmx2::CustomUniformDefinition> &CustomUniformDialog::uniforms() const { return uniformDefinitions; }
201
202bool CustomUniformDialog::setUniformValue(const QString &name, double value) {
203 const int index = uniformIndex(name);
204 if (index < 0)
205 return false;
207 const double bounded = std::clamp(value, uniform.minimum, uniform.maximum);
208 const int position = sliderPosition(uniform, bounded);
209 uniform.value = sliderValue(uniform, position);
210 saveTimer->start();
211 emit uniformsChanged();
212 return true;
213}
214
216 if (libraryDirectory.isEmpty()) {
217 QMessageBox::information(this, tr("Custom Uniforms"), tr("Load a shader library first."));
218 return;
219 }
221 QMessageBox::warning(this, tr("Custom Uniform Limit"), tr("A library can contain at most %1 custom uniforms.").arg(acmx2::ipc::kShaderSelectionMaxCustomUniforms));
222 return;
223 }
224
225 const QString name = nameEdit->text().trimmed();
226 static const QRegularExpression identifier(QStringLiteral("^[A-Za-z_][A-Za-z0-9_]*$"));
227 if (!identifier.match(name).hasMatch() || name.startsWith("gl_") || name.toUtf8().size() >= static_cast<int>(acmx2::ipc::kShaderSelectionMaxUniformName)) {
228 QMessageBox::warning(this, tr("Invalid Uniform Name"), tr("Use a GLSL identifier that does not begin with gl_."));
229 return;
230 }
231 if (uniformIndex(name) >= 0) {
232 QMessageBox::warning(this, tr("Duplicate Uniform"), tr("A custom uniform named '%1' already exists.").arg(name));
233 return;
234 }
235
236 const double minimum = minimumSpin->value();
237 const double maximum = maximumSpin->value();
238 const double step = stepSpin->value();
239 if (maximum <= minimum || step <= 0.0) {
240 QMessageBox::warning(this, tr("Invalid Range"), tr("Maximum must be greater than minimum and step must be positive."));
241 return;
242 }
243 const double steps = std::ceil((maximum - minimum) / step);
244 if (steps > MAX_SLIDER_STEPS) {
245 QMessageBox::warning(this, tr("Too Many Slider Steps"), tr("Increase the step size so the slider has no more than %1 positions.").arg(MAX_SLIDER_STEPS));
246 return;
247 }
248
249 acmx2::CustomUniformDefinition uniform{name, minimum, maximum, step, minimum, static_cast<int>(uniformDefinitions.size())};
250 uniformDefinitions.append(uniform);
251 if (!saveUniforms(true)) {
252 uniformDefinitions.removeLast();
253 return;
254 }
255 nameEdit->clear();
257 emit uniformsChanged();
259}
260
262
264 while (QLayoutItem *item = rowsLayout->takeAt(0)) {
265 if (QWidget *widget = item->widget())
266 widget->deleteLater();
267 delete item;
268 }
269
270 if (uniformDefinitions.isEmpty()) {
271 auto *emptyLabel = new QLabel(tr("No custom uniforms are defined for this library."), rowsWidget);
272 emptyLabel->setAlignment(Qt::AlignCenter);
273 rowsLayout->addWidget(emptyLabel);
274 rowsLayout->addStretch();
275 return;
276 }
277
279 auto *row = new QWidget(rowsWidget);
280 auto *layout = new QGridLayout(row);
281 layout->setContentsMargins(4, 4, 4, 4);
282
283 auto *nameLabel = new QLabel(uniform.name, row);
284 nameLabel->setMinimumWidth(140);
285 auto *slider = new QSlider(Qt::Horizontal, row);
286 slider->setRange(0, sliderStepCount(uniform));
287 slider->setValue(sliderPosition(uniform, uniform.value));
288 auto *valueSpin = new QDoubleSpinBox(row);
289 valueSpin->setDecimals(decimalsForStep(uniform.step));
290 valueSpin->setRange(uniform.minimum, uniform.maximum);
291 valueSpin->setSingleStep(uniform.step);
292 valueSpin->setValue(uniform.value);
293 valueSpin->setKeyboardTracking(false);
294 valueSpin->setMinimumWidth(130);
295 auto *rangeLabel = new QLabel(tr("%1 to %2, step %3").arg(uniform.minimum, 0, 'g', 8).arg(uniform.maximum, 0, 'g', 8).arg(uniform.step, 0, 'g', 8), row);
296 const QString location = uniformLocation(uniform);
297 auto *locationLabel = new QLabel(activeBackend == acmx2::Backend::Acmxvk ? tr("Slot %1: %2").arg(uniform.slot).arg(location) : tr("Slot %1").arg(uniform.slot), row);
298 locationLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
299 auto *copyButton = new QPushButton(tr("Copy"), row);
300 copyButton->setToolTip(tr("Copy the source declaration:\n%1").arg(uniformSourceDefinition(uniform, activeBackend)));
301 auto *deleteButton = new QPushButton(tr("Delete"), row);
302 disableDefaultButton(copyButton);
303 disableDefaultButton(deleteButton);
304
305 layout->addWidget(nameLabel, 0, 0);
306 layout->addWidget(slider, 0, 1);
307 layout->addWidget(valueSpin, 0, 2);
308 layout->addWidget(copyButton, 0, 3);
309 layout->addWidget(deleteButton, 0, 4);
310 layout->addWidget(rangeLabel, 1, 1, 1, 2);
311 layout->addWidget(locationLabel, 1, 3, 1, 2);
312 layout->setColumnStretch(1, 1);
313 rowsLayout->addWidget(row);
314
315 connect(slider, &QSlider::valueChanged, this, [this, name = uniform.name, valueSpin](int position) {
316 const int index = uniformIndex(name);
317 if (index < 0)
318 return;
319 auto &definition = uniformDefinitions[index];
320 definition.value = sliderValue(definition, position);
321 const QSignalBlocker blocker(valueSpin);
322 valueSpin->setValue(definition.value);
323 saveTimer->start();
324 emit uniformsChanged();
325 });
326 connect(valueSpin, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, [this, name = uniform.name, slider, valueSpin](double value) {
327 const int index = uniformIndex(name);
328 if (index < 0)
329 return;
330 auto &definition = uniformDefinitions[index];
331 const int position = sliderPosition(definition, value);
332 definition.value = sliderValue(definition, position);
333 const QSignalBlocker sliderBlocker(slider);
334 const QSignalBlocker spinBlocker(valueSpin);
335 slider->setValue(position);
336 valueSpin->setValue(definition.value);
337 saveTimer->start();
338 emit uniformsChanged();
339 });
340 connect(deleteButton, &QPushButton::clicked, this, [this, name = uniform.name]() { removeUniform(name); });
341 connect(copyButton, &QPushButton::clicked, this, [uniform, backend = activeBackend]() { QGuiApplication::clipboard()->setText(uniformSourceDefinition(uniform, backend)); });
342 }
343 rowsLayout->addStretch();
344}
345
346void CustomUniformDialog::removeUniform(const QString &name) {
347 const int index = uniformIndex(name);
348 if (index < 0)
349 return;
350 const QList<acmx2::CustomUniformDefinition> previousDefinitions = uniformDefinitions;
351 const acmx2::CustomUniformDefinition removed = uniformDefinitions.takeAt(index);
352 if (removed.slot >= 0) {
354 if (uniform.slot > removed.slot)
355 --uniform.slot;
356 }
357 }
358 if (!saveUniforms(true)) {
359 uniformDefinitions = previousDefinitions;
360 return;
361 }
363 emit uniformsChanged();
365}
366
367int CustomUniformDialog::uniformIndex(const QString &name) const {
368 for (int i = 0; i < uniformDefinitions.size(); ++i) {
369 if (uniformDefinitions.at(i).name == name)
370 return i;
371 }
372 return -1;
373}
374
376 if (libraryDirectory.isEmpty())
377 return false;
378 QString error;
380 return true;
381 }
382 if (showError)
383 QMessageBox::warning(this, tr("Could Not Save Custom Uniforms"), error);
384 return false;
385}
void uniformDefinitionsChanged()
QDoubleSpinBox * minimumSpin
int uniformIndex(const QString &name) const
acmx2::Backend activeBackend
void removeUniform(const QString &name)
QList< acmx2::CustomUniformDefinition > uniformDefinitions
QDoubleSpinBox * stepSpin
const QList< acmx2::CustomUniformDefinition > & uniforms() const
bool setUniformValue(const QString &name, double value)
bool loadLibrary(const QString &directory, acmx2::Backend backend, QString *error=nullptr)
QDoubleSpinBox * maximumSpin
CustomUniformDialog(QWidget *parent=nullptr)
bool saveUniforms(bool showError)
constexpr std::uint32_t kShaderSelectionMaxUniformName
constexpr std::uint32_t kShaderSelectionMaxCustomUniforms
bool load_custom_uniforms(const QString &directory, QList< CustomUniformDefinition > &uniforms, QString &error)
Load custom float-uniform controls from library.json.
Backend
Definition backend.hpp:8
bool write_custom_uniforms(const QString &directory, const QList< CustomUniformDefinition > &uniforms, QString &error)
Rewrite custom float-uniform controls while preserving all other JSON fields.
int sliderPosition(const acmx2::CustomUniformDefinition &uniform, double value)
QString uniformSourceDefinition(const acmx2::CustomUniformDefinition &uniform, acmx2::Backend backend)
double sliderValue(const acmx2::CustomUniformDefinition &uniform, int position)
void disableDefaultButton(QPushButton *button)
QString uniformLocation(const acmx2::CustomUniformDefinition &uniform)
void configureDoubleSpinBox(QDoubleSpinBox *spin)
int sliderStepCount(const acmx2::CustomUniformDefinition &uniform)