ACMX 2.139.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
ACMX2/interface/main_window.cpp
Go to the documentation of this file.
1#include "main_window.hpp"
2#include "audio-window.hpp"
3#include "custom-uniforms.hpp"
4#include "custom_style.hpp"
6#include "find-shader.hpp"
7#include "library-builder.hpp"
8#include "metadata-viewer.hpp"
9#include "settings.hpp"
10#include "shader-manifest.hpp"
12#include "uniform-reference.hpp"
13#include <QApplication>
14#include <QCheckBox>
15#include <QClipboard>
16#include <QColorDialog>
17#include <QComboBox>
18#include <QDataStream>
19#include <QDateTime>
20#include <QDebug>
21#include <QDialog>
22#include <QDialogButtonBox>
23#include <QDir>
24#include <QFile>
25#include <QFileDialog>
26#include <QFileInfo>
27#include <QFormLayout>
28#include <QFrame>
29#include <QGuiApplication>
30#include <QHBoxLayout>
31#include <QHeaderView>
32#include <QIcon>
33#include <QInputDialog>
34#include <QLabel>
35#include <QLayout>
36#include <QLineEdit>
37#include <QLocale>
38#include <QMessageBox>
39#include <QPlainTextEdit>
40#include <QProcess>
41#include <QPushButton>
42#include <QRegularExpression>
43#include <QSaveFile>
44#include <QSpinBox>
45#include <QStandardPaths>
46#include <QTabWidget>
47#include <QTextStream>
48#include <QTimer>
49#include <QTreeWidgetItem>
50#include <QVBoxLayout>
51#include <algorithm>
52#include <array>
53#include <cstring>
54#include <filesystem>
55#include <functional>
56#include <random>
57#include <sstream>
58#ifdef _WIN32
59#ifndef NOMINMAX
60#define NOMINMAX
61#endif
62#include <windows.h>
63#endif
64#if defined(__linux__) || defined(__APPLE__)
65#include <fcntl.h>
66#include <sys/mman.h>
67#include <sys/stat.h>
68#include <sys/types.h>
69#include <unistd.h>
70#endif
71
72namespace {
73 constexpr int RECENT_LIBRARY_LIMIT = 10;
74
75 bool take_process_output_line(QString &buffer, QString &line) {
76 const qsizetype newline = buffer.indexOf('\n');
77 const qsizetype carriage = buffer.indexOf('\r');
78 qsizetype separator = newline;
79 if (separator < 0 || (carriage >= 0 && carriage < separator)) {
80 separator = carriage;
81 }
82 if (separator < 0) {
83 return false;
84 }
85
86 line = buffer.left(separator);
87 qsizetype consumed = 1;
88 if (buffer.at(separator) == QLatin1Char('\r') && separator + 1 < buffer.size() && buffer.at(separator + 1) == QLatin1Char('\n')) {
89 consumed = 2;
90 }
91 buffer.remove(0, separator + consumed);
92 return true;
93 }
94
95 bool is_stable_diffusion_diagnostic(const QString &line) {
96 QString normalized = line;
97 static const QRegularExpression ANSI_ESCAPE(QStringLiteral("\\x1b\\[[0-9;?]*[ -/]*[@-~]"));
98 normalized.remove(ANSI_ESCAPE);
99 normalized = normalized.trimmed();
100 if (normalized.startsWith("[INFO ]") || normalized.startsWith("[DEBUG]") || normalized.startsWith("[TRACE]")) {
101 return true;
102 }
103 if (normalized.startsWith(QLatin1Char('|')) && normalized.contains(QRegularExpression(QStringLiteral("\\d+\\s*/\\s*\\d+")))) {
104 return true;
105 }
106 return false;
107 }
108
109 QString shellQuote(const QString &value) {
110#ifdef _WIN32
111 if (value.isEmpty()) {
112 return QStringLiteral("\"\"");
113 }
114
115 QString quoted = QStringLiteral("\"");
116 qsizetype backslash_count = 0;
117 for (const QChar character : value) {
118 if (character == QLatin1Char('\\')) {
119 ++backslash_count;
120 continue;
121 }
122 if (character == QLatin1Char('"')) {
123 quoted += QString(backslash_count * 2 + 1, QLatin1Char('\\'));
124 quoted += character;
125 backslash_count = 0;
126 continue;
127 }
128 quoted += QString(backslash_count, QLatin1Char('\\'));
129 backslash_count = 0;
130 quoted += character;
131 }
132 quoted += QString(backslash_count * 2, QLatin1Char('\\'));
133 quoted += QLatin1Char('"');
134 return quoted;
135#else
136 if (value.isEmpty()) {
137 return "''";
138 }
139 QString out = value;
140 out.replace("'", "'\\''");
141 return "'" + out + "'";
142#endif
143 }
144
145 QString buildShellCommand(const QStringList &envAssignments, const QString &program, const QStringList &arguments) {
146 QStringList parts;
147 parts.reserve(envAssignments.size() + 1 + arguments.size());
148 for (const QString &entry : envAssignments) {
149 int eq = entry.indexOf('=');
150 if (eq <= 0) {
151 continue;
152 }
153 QString key = entry.left(eq);
154 QString value = entry.mid(eq + 1);
155#ifdef _WIN32
156 parts << (QStringLiteral("set \"") + key + QLatin1Char('=') + value + QStringLiteral("\" &&"));
157#else
158 parts << (key + "=" + shellQuote(value));
159#endif
160 }
161 parts << shellQuote(program);
162 for (const QString &arg : arguments) {
163 parts << shellQuote(arg);
164 }
165 return parts.join(' ');
166 }
167
168 void replace_file(const std::filesystem::path &source, const std::filesystem::path &destination, std::error_code &error) {
169#ifdef _WIN32
170 if (MoveFileExW(source.c_str(), destination.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != FALSE) {
171 error.clear();
172 return;
173 }
174 error = std::error_code(static_cast<int>(GetLastError()), std::system_category());
175#else
176 std::filesystem::rename(source, destination, error);
177#endif
178 }
179
180#ifdef __linux__
181 QStringList defaultLinuxRunEnvAssignments() {
182 QStringList envAssignments;
183 QString uid = QString::number(getuid());
184 QString userRunPath = "/run/user/" + uid;
185 QByteArray display = qgetenv("DISPLAY");
186 QByteArray waylandDisplay = qgetenv("WAYLAND_DISPLAY");
187 QByteArray sessionType = qgetenv("XDG_SESSION_TYPE");
188 if (!waylandDisplay.isEmpty() && sessionType == "wayland") {
189 envAssignments << "SDL_VIDEODRIVER=wayland";
190 } else if (!display.isEmpty()) {
191 envAssignments << "SDL_VIDEODRIVER=x11";
192 } else if (!waylandDisplay.isEmpty()) {
193 envAssignments << "SDL_VIDEODRIVER=wayland";
194 }
195 if (QDir(userRunPath).exists()) {
196 envAssignments << ("XDG_RUNTIME_DIR=" + userRunPath);
197 envAssignments << ("PULSE_SERVER=unix:" + userRunPath + "/pulse/native");
198 }
199 envAssignments << "vblank_mode=0";
200 return envAssignments;
201 }
202#endif
203
205 QString dirPath = QCoreApplication::applicationDirPath();
206#ifdef BUILD_BUNDLE
207 return dirPath + "/../Helpers";
208#else
209 if (QFileInfo::exists(dirPath + "/data/win-icon.png"))
210 return dirPath;
211 const QString installedPath = QDir::cleanPath(dirPath + "/../share/acmx2");
212 if (QFileInfo::exists(installedPath + "/data/win-icon.png"))
213 return installedPath;
214 return dirPath;
215#endif
216 }
217
218 QString resolve_acmxvk_shader_compiler(QString &error) {
219 error.clear();
220 QSettings settings("LostSideDead");
221 const QString mode = settings.value(acmx2::backend_settings_key(acmx2::Backend::Acmxvk, "shader_compiler_mode"), "auto").toString();
222 if (mode == QStringLiteral("custom")) {
223 QString compiler = settings.value(acmx2::backend_settings_key(acmx2::Backend::Acmxvk, "shader_compiler_path")).toString().trimmed();
224 if (compiler.isEmpty()) {
225 error = QStringLiteral("The custom ACMXVK shader compiler path is empty. Select "
226 "one in Properties (Ctrl+,).");
227 return {};
228 }
229 if (QFileInfo(compiler).isRelative()) {
230 const QString resolved = QStandardPaths::findExecutable(compiler);
231 if (!resolved.isEmpty())
232 compiler = resolved;
233 }
234 const QFileInfo compilerInfo(compiler);
235 if (!compilerInfo.isFile() || !compilerInfo.isExecutable()) {
236 error = QStringLiteral("The configured ACMXVK shader compiler is not an "
237 "executable file: %1")
238 .arg(compiler);
239 return {};
240 }
241 return compilerInfo.absoluteFilePath();
242 }
243
244 QString compiler;
245#ifdef _WIN32
246 const QString compilerName = QStringLiteral("glslc.exe");
247 const QFileInfo bundledCompiler(QDir(QCoreApplication::applicationDirPath()).filePath(compilerName));
248 if (bundledCompiler.isFile() && bundledCompiler.isExecutable())
249 compiler = bundledCompiler.absoluteFilePath();
250#else
251 const QString compilerName = QStringLiteral("glslc");
252#endif
253 if (compiler.isEmpty())
254 compiler = QStandardPaths::findExecutable(compilerName);
255 if (compiler.isEmpty()) {
256 const QString sdk = QString::fromLocal8Bit(qgetenv("VULKAN_SDK"));
257 const QString sdkCompiler = QDir(sdk).filePath(QStringLiteral("bin/") + compilerName);
258 if (!sdk.isEmpty() && QFileInfo(sdkCompiler).isExecutable())
259 compiler = sdkCompiler;
260 }
261 if (compiler.isEmpty()) {
262 error = QStringLiteral("glslc was not found in PATH or VULKAN_SDK. Select a custom "
263 "compiler in Properties (Ctrl+,).");
264 }
265 return compiler;
266 }
267
269#ifdef _WIN32
270 const QFileInfo bundled_server(QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("sd-server.exe")));
271 if (bundled_server.isFile()) {
272 return bundled_server.absoluteFilePath();
273 }
274 return QStringLiteral("sd-server.exe");
275#else
276 const QFileInfo bundled_server(QStringLiteral("/app/bin/sd-server"));
277 if (bundled_server.isExecutable()) {
278 return bundled_server.absoluteFilePath();
279 }
280 return QStringLiteral("sd-server");
281#endif
282 }
283
284 QString resolve_backend_assets_path(acmx2::Backend backend, const QString &executable, const QString &libraryPath) {
285 if (backend == acmx2::Backend::Acmx2)
286 return resolveAssetsPath();
287
288 const QString applicationDir = QCoreApplication::applicationDirPath();
289#ifdef BUILD_BUNDLE
290 const QString bundleResources = QDir::cleanPath(applicationDir + "/../Resources/acmxvk");
291 if (QFileInfo(bundleResources).isDir())
292 return bundleResources;
293#endif
294 QStringList candidates;
295 QString resolvedExecutable = executable;
296 if (QFileInfo(resolvedExecutable).isRelative()) {
297 const QString pathExecutable = QStandardPaths::findExecutable(resolvedExecutable);
298 if (!pathExecutable.isEmpty())
299 resolvedExecutable = pathExecutable;
300 }
301 const QFileInfo executableInfo(resolvedExecutable);
302 if (!executableInfo.absolutePath().isEmpty()) {
303 candidates << QDir::cleanPath(executableInfo.absolutePath() + "/../share/acmxvk");
304 }
305 if (!libraryPath.isEmpty())
306 candidates << QDir::cleanPath(QFileInfo(libraryPath).absolutePath());
307 candidates << QDir::cleanPath(applicationDir + "/../share/acmxvk") << QStringLiteral("/usr/local/share/acmxvk") << QStringLiteral("/opt/homebrew/share/acmxvk") << QStringLiteral("/usr/share/acmxvk");
308 for (const QString &candidate : candidates) {
309 if (QFileInfo(candidate + "/data").isDir())
310 return candidate;
311 }
312
313 // --path only requires a readable directory. ACMXVK can still use the
314 // explicitly selected shader library if no installed data tree exists.
315 return applicationDir;
316 }
317
318 bool is_acmxvk_source_library(const QString &libraryPath, QString &error) {
319 error.clear();
320 const std::optional<acmx2::ShaderLibraryType> type = acmx2::shader_manifest_library_type(libraryPath, error);
321 if (!error.isEmpty())
322 return false;
323 if (type)
324 return *type == acmx2::ShaderLibraryType::Source;
325
326 // Legacy manifests may not carry library_type. Infer source libraries
327 // from GLSL entries while continuing to accept old SPIR-V manifests.
328 QStringList entries;
329 if (!acmx2::load_shader_manifest(libraryPath, entries, error))
330 return false;
331 return std::any_of(entries.cbegin(), entries.cend(), [](const QString &entry) { return entry.endsWith(".frag", Qt::CaseInsensitive) || entry.endsWith(".comp", Qt::CaseInsensitive); });
332 }
333
334 QString acmxvk_build_directory(const QString &sourceLibrary) { return QDir(sourceLibrary).filePath(QStringLiteral(".acmxvk-build")); }
335
336 QString acmxvk_runtime_shader_name(const QString &sourceName) { return sourceName.endsWith(".spv", Qt::CaseInsensitive) ? sourceName : sourceName + QStringLiteral(".spv"); }
337
339
340 AcmxvkBuildState acmxvk_shader_build_state(const QString &source_library, const QString &source_name) {
341 const QString runtime_library = acmxvk_build_directory(source_library);
342 if (!acmx2::shader_manifest_exists(runtime_library))
344
345 const QFileInfo source_file(QDir(source_library).filePath(source_name));
346 const QFileInfo runtime_file(QDir(runtime_library).filePath(acmxvk_runtime_shader_name(source_name)));
347 if (!runtime_file.isFile())
349 if (source_file.isFile() && runtime_file.lastModified() < source_file.lastModified()) {
351 }
353 }
354
355 bool acmxvk_runtime_manifest_matches(const QString &source_library, const QString &runtime_library, QString &error) {
356 QStringList source_entries;
357 QStringList runtime_entries;
358 if (!acmx2::load_shader_manifest(source_library, source_entries, error) || !acmx2::load_shader_manifest(runtime_library, runtime_entries, error)) {
359 return false;
360 }
361
362 QStringList expected_entries;
363 expected_entries.reserve(source_entries.size());
364 for (const QString &entry : source_entries)
365 expected_entries.append(acmxvk_runtime_shader_name(entry));
366 if (runtime_entries != expected_entries) {
367 error = QObject::tr("The ACMXVK runtime manifest does not match the source "
368 "shader list. Choose Playback > Build before running.");
369 return false;
370 }
371
372 bool uniformMetadataMatches = false;
373 if (!acmx2::custom_uniform_metadata_matches(source_library, runtime_library, uniformMetadataMatches, error)) {
374 return false;
375 }
376 if (!uniformMetadataMatches) {
377 error = QObject::tr("The ACMXVK runtime custom-uniform metadata is out of date. "
378 "Choose Playback > Build before running.");
379 return false;
380 }
381 return true;
382 }
383
384 bool resolve_acmxvk_runtime_library(const QString &selectedLibrary, QString &runtimeLibrary, QString &error) {
385 error.clear();
386 runtimeLibrary = selectedLibrary;
387 if (!is_acmxvk_source_library(selectedLibrary, error))
388 return error.isEmpty();
389
390 runtimeLibrary = acmxvk_build_directory(selectedLibrary);
391 if (!acmx2::shader_manifest_exists(runtimeLibrary)) {
392 error = QObject::tr("The ACMXVK source library has not been built yet. "
393 "Choose Playback > Build first.\n\nExpected output: %1")
394 .arg(runtimeLibrary);
395 return false;
396 }
397 const std::optional<acmx2::ShaderLibraryType> type = acmx2::shader_manifest_library_type(runtimeLibrary, error);
398 if (!error.isEmpty())
399 return false;
400 if (type && *type != acmx2::ShaderLibraryType::Runtime) {
401 error = QObject::tr("Compiled output is not an ACMXVK runtime library: %1").arg(runtimeLibrary);
402 return false;
403 }
404 QStringList sourceEntries;
405 if (!acmx2::load_shader_manifest(selectedLibrary, sourceEntries, error))
406 return false;
407 if (!acmxvk_runtime_manifest_matches(selectedLibrary, runtimeLibrary, error)) {
408 return false;
409 }
410 for (const QString &sourceEntry : sourceEntries) {
411 const QFileInfo sourceFile(QDir(selectedLibrary).filePath(sourceEntry));
412 const QFileInfo runtimeFile(QDir(runtimeLibrary).filePath(acmxvk_runtime_shader_name(sourceEntry)));
413 if (!runtimeFile.isFile() || runtimeFile.lastModified() < sourceFile.lastModified()) {
414 error = QObject::tr("The ACMXVK build is missing or older than %1. "
415 "Choose Playback > Build before running.")
416 .arg(sourceEntry);
417 return false;
418 }
419 }
420 return true;
421 }
422
424 QSettings settings("LostSideDead", "acmx2");
425 return settings.value("interface/texture_cache_array", false).toBool();
426 }
427
428 QSize storedResolution(QSettings &settings, const QString &key, const QSize &fallback, bool defaultIsEmpty) {
429 const QString text = settings.value(key).toString().trimmed();
430 if (text.compare("Default", Qt::CaseInsensitive) == 0) {
431 return defaultIsEmpty ? QSize(0, 0) : fallback;
432 }
433
434 static const QRegularExpression resolutionPattern(R"(^\s*(\d+)\s*[xX]\s*(\d+)\s*$)");
435 const QRegularExpressionMatch match = resolutionPattern.match(text);
436 if (!match.hasMatch()) {
437 return fallback;
438 }
439
440 const int width = match.captured(1).toInt();
441 const int height = match.captured(2).toInt();
442 return width > 0 && height > 0 ? QSize(width, height) : fallback;
443 }
444
445 bool hasPositiveResolution(const QSize &resolution) { return resolution.width() > 0 && resolution.height() > 0; }
446
447 QString shaderCacheFilename(const QString &libraryPath, int cacheSize, bool useArray) {
448 std::error_code ec;
449 const std::filesystem::path libraryFsPath(libraryPath.toStdString());
450 const std::filesystem::path absoluteLibrary = std::filesystem::absolute(libraryFsPath, ec);
451 std::string key = ec ? libraryPath.toStdString() : absoluteLibrary.lexically_normal().string();
452 key += "|s=" + std::to_string(cacheSize);
453 key += "|a=" + std::to_string(useArray ? 1 : 0);
454 std::ostringstream nameStream;
455 nameStream << ".shader_cache_" << std::hex << std::hash<std::string>{}(key);
456 return QString::fromStdString(nameStream.str());
457 }
458
459 QString resolveShaderCachePath(const QString &libraryPath, int cacheSize, bool useArray) {
460 const QString assets = resolveAssetsPath();
461 const QString filename = shaderCacheFilename(libraryPath, cacheSize, useArray);
462
463 // Mirror ShaderLibrary::shaderCacheFilePath: prefer cache in assets dir,
464 // then fall back to the library directory itself (acmx2 writes there when
465 // assets isn't writable).
466 const QString assetsCache = assets + "/" + filename;
467 const QString libCache = libraryPath + "/" + filename;
468 if (QFileInfo::exists(assetsCache))
469 return assetsCache;
470 if (QFileInfo::exists(libCache))
471 return libCache;
472 return assetsCache;
473 }
474
475 // Parse the shader cache file produced by ShaderLibrary::buildShaderCache().
476 // Returns a map of shader stem -> failed flag. Empty on missing/invalid cache.
477 QHash<QString, bool> parseShaderCacheStatus(const QString &cachePath) {
478 QHash<QString, bool> result;
479 QFile f(cachePath);
480 if (!f.open(QIODevice::ReadOnly))
481 return result;
482
483 auto readU32 = [&](quint32 &v) -> bool { return f.read(reinterpret_cast<char *>(&v), sizeof(v)) == qint64(sizeof(v)); };
484 auto readU64 = [&](quint64 &v) -> bool { return f.read(reinterpret_cast<char *>(&v), sizeof(v)) == qint64(sizeof(v)); };
485 auto readU8 = [&](quint8 &v) -> bool { return f.read(reinterpret_cast<char *>(&v), sizeof(v)) == qint64(sizeof(v)); };
486 auto readStr = [&](QString &out) -> bool {
487 quint32 len = 0;
488 if (!readU32(len))
489 return false;
490 QByteArray buf = f.read(len);
491 if (quint32(buf.size()) != len)
492 return false;
493 out = QString::fromUtf8(buf);
494 return true;
495 };
496 auto skipBytes = [&](quint32 n) -> bool { return f.skip(n) == qint64(n); };
497
498 constexpr quint32 CACHE_MAGIC = 0x53484452;
499 constexpr quint32 CACHE_VERSION = 4;
500
501 quint32 magic = 0, version = 0;
502 if (!readU32(magic) || !readU32(version))
503 return result;
504 if (magic != CACHE_MAGIC || version != CACHE_VERSION)
505 return result;
506
507 QString tmp;
508 if (!readStr(tmp))
509 return result; // gl_renderer
510 if (!readStr(tmp))
511 return result; // gl_version
512
513 quint8 dual_mode = 0;
514 if (!readU8(dual_mode))
515 return result;
516
517 quint32 count = 0;
518 if (!readU32(count))
519 return result;
520
521 for (quint32 i = 0; i < count; ++i) {
522 QString name;
523 if (!readStr(name))
524 return result;
525 quint8 shader_kind = 0;
526 if (!readU8(shader_kind) || shader_kind > 2)
527 return result;
528 quint8 failed_flag = 0;
529 if (!readU8(failed_flag))
530 return result;
531 quint64 source_hash = 0;
532 if (!readU64(source_hash))
533 return result;
534 quint32 fmt2d = 0, sz2d = 0, fmt3d = 0, sz3d = 0;
535 if (!readU32(fmt2d) || !readU32(sz2d) || !skipBytes(sz2d))
536 return result;
537 if (!readU32(fmt3d) || !readU32(sz3d) || !skipBytes(sz3d))
538 return result;
539 result.insert(name, failed_flag != 0);
540 }
541 return result;
542 }
543
544 QString formatLastModified(const QDateTime &dt) {
545 if (!dt.isValid())
546 return QStringLiteral("-");
547 return dt.toLocalTime().toString(QStringLiteral("yyyy-MM-dd HH:mm"));
548 }
549} // namespace
550
552 lastFoundIndex = -1;
553 lastSearchText = QString();
554 process = new QProcess(this);
555 auto updateShaderMenuState = [this](QProcess::ProcessState state) {
556 const bool running = (state == QProcess::Running);
557 if (backendMenu)
558 backendMenu->setEnabled(!running);
559 if (listMenu_new) {
560 listMenu_new->setEnabled(!running);
561 }
562 if (listMenu_shader) {
563 listMenu_shader->setEnabled(!running);
564 }
566 libraryBuilderAction->setEnabled(!running);
567 }
568 if (listMenu_remove) {
569 listMenu_remove->setEnabled(!running);
570 }
571 if (listMenu_up) {
572 listMenu_up->setEnabled(!running);
573 }
574 if (listMenu_down) {
575 listMenu_down->setEnabled(!running);
576 }
577 if (listMenu_shuffle) {
578 listMenu_shuffle->setEnabled(!running);
579 }
580 if (listMenu_sort) {
581 listMenu_sort->setEnabled(!running);
582 }
584 listMenu_set_current->setEnabled(running);
585 }
586 };
587 connect(process, &QProcess::stateChanged, this, updateShaderMenuState);
588 updateShaderMenuState(process->state());
589 connect(process, &QProcess::readyReadStandardOutput, this, [this]() {
590 stdoutBuffer += QString::fromLocal8Bit(process->readAllStandardOutput());
591 QString line;
592 while (take_process_output_line(stdoutBuffer, line)) {
593 appendOutputRunLog(line);
594 if (active_backend == acmx2::Backend::Acmxvk && stable_diffusion_enabled && is_stable_diffusion_diagnostic(line)) {
595 continue;
596 }
597 this->Write(line + "<br>");
598 }
599 });
600
601 connect(process, &QProcess::readyReadStandardError, this, [this]() {
602 auto writeStderrLine = [this](const QString &line) {
603 if (line.contains("GStreamer"))
604 return;
605 if (active_backend == acmx2::Backend::Acmxvk && stable_diffusion_enabled && is_stable_diffusion_diagnostic(line)) {
606 return;
607 }
608 if (line.contains("[ WARN:") || line.contains("[WARN "))
609 this->Write("<b style='color:#ccaa00;'>Warning:</b> " + line + "<br>");
610 else
611 this->Write("<b style='color:red;'>Error:</b> " + line + "<br>");
612 };
613
614 stderrBuffer += QString::fromLocal8Bit(process->readAllStandardError());
615 QString line;
616 while (take_process_output_line(stderrBuffer, line)) {
617 appendOutputRunLog(line);
618 writeStderrLine(line);
619 }
620 if (stderrBuffer.size() > 4096) {
622 writeStderrLine(stderrBuffer);
623 stderrBuffer.clear();
624 }
625 });
626
627 connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
628 if (!stdoutBuffer.isEmpty() && !(active_backend == acmx2::Backend::Acmxvk && stable_diffusion_enabled && is_stable_diffusion_diagnostic(stdoutBuffer))) {
629 this->Write(stdoutBuffer + "<br>");
630 }
632 stdoutBuffer.clear();
633 if (!stderrBuffer.isEmpty() && !stderrBuffer.contains("GStreamer") && !(active_backend == acmx2::Backend::Acmxvk && stable_diffusion_enabled && is_stable_diffusion_diagnostic(stderrBuffer))) {
634 if (stderrBuffer.contains("[ WARN:") || stderrBuffer.contains("[WARN "))
635 this->Write("<b style='color:#ccaa00;'>Warning:</b> " + stderrBuffer + "<br>");
636 else
637 this->Write("<b style='color:red;'>Error:</b> " + stderrBuffer + "<br>");
638 }
640 stderrBuffer.clear();
641 QString text;
642 QTextStream stream(&text);
643 stream << acmx2::backend_name(active_backend) << ": Exited with Code: " << exitCode;
644 Log(text + "<br>");
645 finishOutputRunLog(exitCode, exitStatus);
646 play_stop->setEnabled(false);
647
648 if (exitStatus == QProcess::CrashExit) {
649 qDebug() << acmx2::backend_name(active_backend) << "engine crashed.";
650 Log("<b style='color:red;'>" + acmx2::backend_name(active_backend) + " engine crashed.</b><br>");
651 }
652
653 // Refresh the shader tree's compile-health column now that
654 // the child process has (re)written the binary shader cache.
656
657 const bool finishedBuildProcess = cacheBuildInProgress;
659 const PendingAcmxvkAction resume_action = pending_acmxvk_action;
660 const QString pruneLibraryPath = acmxvkPruneLibraryPath;
664 if (exitCode == 0) {
665 Log(tr("ACMXVK build ready: %1").arg(acmxvk_build_directory(shader_path)));
666 } else {
667 Log(tr("<b style='color:red;'>ACMXVK build failed "
668 "with exit code %1.</b>")
669 .arg(exitCode));
670 }
671 }
672 cacheBuildInProgress = false;
674 if (!pruneLibraryPath.isEmpty() && exitCode == 0 && exitStatus == QProcess::NormalExit) {
675 QStringList sourceShaders;
676 QString manifestError;
677 if (!acmx2::load_shader_manifest(pruneLibraryPath, sourceShaders, manifestError)) {
678 Log(tr("<b style='color:red;'>Broken sources were "
679 "pruned, but the source manifest could not "
680 "be read: %1</b>")
681 .arg(manifestError.toHtmlEscaped()));
682 QMessageBox::warning(this,
683 tr("Remove Broken Shaders"),
684 tr("Broken source files were deleted, but the "
685 "source manifest could not be updated.\n\n%1")
686 .arg(manifestError));
687 } else {
688 QStringList retainedShaders;
689 int removedCount = 0;
690 const QDir sourceDirectory(pruneLibraryPath);
691 for (const QString &shader : sourceShaders) {
692 const QString suffix = QFileInfo(shader).suffix().toLower();
693 const bool sourceEntry = suffix == QStringLiteral("frag") || suffix == QStringLiteral("comp");
694 if (sourceEntry && !QFileInfo(sourceDirectory.filePath(shader)).isFile()) {
695 ++removedCount;
696 } else {
697 retainedShaders.append(shader);
698 }
699 }
700
701 if (removedCount > 0 && !acmx2::write_shader_manifest(pruneLibraryPath, retainedShaders, manifestError)) {
702 Log(tr("<b style='color:red;'>Broken sources "
703 "were pruned, but the source manifest "
704 "could not be updated: %1</b>")
705 .arg(manifestError.toHtmlEscaped()));
706 QMessageBox::warning(this,
707 tr("Remove Broken Shaders"),
708 tr("%1 source file(s) were permanently "
709 "deleted, but library.json could not be "
710 "updated.\n\n%2")
711 .arg(removedCount)
712 .arg(manifestError));
713 } else {
714 if (shader_path == pruneLibraryPath)
715 loadShaders(pruneLibraryPath, true);
716 Log(tr("Remove Broken completed: %1 source "
717 "shader(s) permanently deleted.")
718 .arg(removedCount));
719 QMessageBox::information(this,
720 tr("Remove Broken Shaders"),
721 removedCount > 0 ? tr("Removed %1 broken source shader(s) "
722 "and updated library.json.\n\n"
723 "This deletion cannot be undone.")
724 .arg(removedCount)
725 : tr("The build completed and no broken "
726 "source shaders were found."));
727 }
728 }
729 }
730 if (active_backend == acmx2::Backend::Acmxvk && exitCode == 0 && exitStatus == QProcess::NormalExit && resume_action != PendingAcmxvkAction::None) {
731 Log(tr("ACMXVK build succeeded; resuming the requested "
732 "action."));
733 QTimer::singleShot(0, this, [this, resume_action]() {
734 if (resume_action == PendingAcmxvkAction::RunSelected) {
735 runSelected();
736 } else if (resume_action == PendingAcmxvkAction::RunAll) {
737 runAll();
738 } else if (resume_action == PendingAcmxvkAction::CopyCommand) {
739 copyCommand();
740 }
741 });
742 }
743 }
744
745 // Optional post-process: convert the produced HLG HDR file
746 // to HDR10 via ffmpeg and stream its output to the log.
747 if (!finishedBuildProcess && convert_to_hdr10 && exitCode == 0 && !output_file.isEmpty() && QFileInfo::exists(output_file)) {
749 }
750 });
751
752 hdr10Process = new QProcess(this);
753 connect(hdr10Process, &QProcess::readyReadStandardOutput, this, [this]() {
754 QString output = QString::fromUtf8(hdr10Process->readAllStandardOutput());
755 output.replace("\n", "<br>");
756 this->Write(output);
757 });
758 connect(hdr10Process, &QProcess::readyReadStandardError, this, [this]() {
759 QString output = QString::fromUtf8(hdr10Process->readAllStandardError());
760 output.replace("\n", "<br>");
761 // ffmpeg writes progress to stderr; render in a neutral colour rather
762 // than the alarming red used for acmx2 errors.
763 this->Write("<span style='color:#88aaff;'>" + output + "</span>");
764 });
765 connect(hdr10Process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus) {
766 QString text;
767 QTextStream stream(&text);
768 stream << "ffmpeg (HDR10): Exited with Code: " << exitCode;
769 Log(text + "<br>");
770 play_stop->setEnabled(false);
771 });
772
773 setStyleSheet(" QMainWindow { background-color: rgb(0,0,0); }");
774 camera_index = 0;
775 camera_res = QSize(1280, 720);
776 screen_res = QSize(0, 0);
777 setGeometry(150, 150, 1280, 720);
778 setWindowTitle("ACMX2 - Interface");
779 QMenuBar *menuBarPtr = menuBar();
780
781 menuBar()->setNativeMenuBar(false);
782 fileMenu = menuBarPtr->addMenu(tr("File"));
783 cameraMenu = menuBarPtr->addMenu(tr("Session"));
784 backendMenu = menuBarPtr->addMenu(tr("Backend"));
785 playbackMenu = menuBarPtr->addMenu(tr("Playback"));
786 runMenu = menuBarPtr->addMenu(tr("Run"));
787 listMenu = menuBarPtr->addMenu(tr("List"));
788 viewMenu = menuBarPtr->addMenu(tr("View"));
789 helpMenu = menuBarPtr->addMenu(tr("Help"));
790 backendActionGroup = new QActionGroup(this);
791 backendActionGroup->setExclusive(true);
792 backendAcmx2Action = backendMenu->addAction(tr("ACMX2"));
793 backendAcmx2Action->setCheckable(true);
794 backendAcmx2Action->setChecked(true);
796 backendAcmxvkAction = backendMenu->addAction(tr("ACMXVK"));
797 backendAcmxvkAction->setCheckable(true);
799 connect(backendAcmx2Action, &QAction::triggered, this, [this]() { set_backend(acmx2::Backend::Acmx2); });
800 connect(backendAcmxvkAction, &QAction::triggered, this, [this]() { set_backend(acmx2::Backend::Acmxvk); });
801 stayOnTopAction = new QAction(tr("Stay on Top"), this);
802 stayOnTopAction->setShortcut(QKeySequence("Ctrl+Alt+T"));
803 stayOnTopAction->setCheckable(true);
804 stayOnTopAction->setChecked(false);
805 connect(stayOnTopAction, &QAction::toggled, this, [this](bool checked) {
806 if (checked) {
807 setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
808 } else {
809 setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint);
810 }
811 show();
812 if (checked && QGuiApplication::platformName() == "wayland") {
813 Log("Stay on Top may not work on Wayland. Launch with QT_QPA_PLATFORM=xcb for X11 support.");
814 }
815 });
816 viewMenu->addAction(stayOnTopAction);
817 QAction *metadataAction = new QAction(tr("Media Metadata Viewer..."), this);
818 metadataAction->setShortcut(QKeySequence("Ctrl+Alt+V"));
819 connect(metadataAction, &QAction::triggered, this, &MainWindow::menuMetadataViewer);
820 viewMenu->addSeparator();
821 viewMenu->addAction(metadataAction);
822 fileMenu_loadLibrary = new QAction(tr("Load Library..."), this);
823 fileMenu_loadLibrary->setShortcut(QKeySequence::Open);
824 connect(fileMenu_loadLibrary, &QAction::triggered, this, &MainWindow::menuLoadLibrary);
825 fileMenu->addAction(fileMenu_loadLibrary);
826 loadRecentMenu = fileMenu->addMenu(tr("Load Recent"));
827 loadRecentMenu->menuAction()->setShortcut(QKeySequence("Ctrl+Shift+O"));
828 connect(loadRecentMenu, &QMenu::aboutToShow, this, &MainWindow::updateRecentLibrariesMenu);
830 fileMenu->addSeparator();
831 fileMenu_prop = new QAction(tr("Properties"), this);
832 fileMenu_prop->setShortcut(QKeySequence("Ctrl+,"));
833 fileMenu->addAction(fileMenu_prop);
834 connect(fileMenu_prop, &QAction::triggered, this, &MainWindow::fileOpenProp);
835 fileMenu->addSeparator();
836 fileMenu_exit = new QAction(tr("Exit"), this);
837 fileMenu_exit->setShortcut(QKeySequence::Quit);
838 connect(fileMenu_exit, &QAction::triggered, this, &MainWindow::fileExit);
839 fileMenu->addAction(fileMenu_exit);
840 cameraSet = new QAction(tr("Session Properties"), this);
841 cameraSet->setShortcut(QKeySequence("Ctrl+Shift+P"));
842 connect(cameraSet, &QAction::triggered, this, &MainWindow::cameraSettings);
843 cameraMenu->addAction(cameraSet);
844 audioSet = new QAction(tr("Audio Settings"), this);
845 audioSet->setShortcut(QKeySequence("Ctrl+Shift+A"));
846 connect(audioSet, &QAction::triggered, this, &MainWindow::menuAudioSettings);
847 cameraMenu->addAction(audioSet);
848 gpuFilterAction = new QAction(tr("GPU Filter Settings"), this);
849 gpuFilterAction->setShortcut(QKeySequence("Ctrl+Shift+G"));
850 connect(gpuFilterAction, &QAction::triggered, this, &MainWindow::menuGPUFilterSettings);
851 cameraMenu->addAction(gpuFilterAction);
852 deepDreamAction = new QAction(tr("Deep Dream Settings..."), this);
853 deepDreamAction->setShortcut(QKeySequence("Ctrl+Shift+D"));
854 connect(deepDreamAction, &QAction::triggered, this, &MainWindow::menuDeepDreamSettings);
855 cameraMenu->addAction(deepDreamAction);
856 stableDiffusionAction = new QAction(tr("Stable Diffusion Settings..."), this);
857 connect(stableDiffusionAction, &QAction::triggered, this, &MainWindow::menuStableDiffusionSettings);
859 cameraMenu->addSeparator();
860 styleSheetAction = new QAction(tr("Use Custom Style"), this);
861 styleSheetAction->setShortcut(QKeySequence("Ctrl+Shift+T"));
862 styleSheetAction->setCheckable(true);
863 styleSheetAction->setChecked(false);
864 connect(styleSheetAction, &QAction::triggered, this, &MainWindow::openCustomStyleEditor);
865 cameraMenu->addAction(styleSheetAction);
866 runMenu_select = new QAction(tr("Run Selected"), this);
867 runMenu_select->setShortcut(QKeySequence("F5"));
868 connect(runMenu_select, &QAction::triggered, this, &MainWindow::runSelected);
869 runMenu->addAction(runMenu_select);
870 runMenu->addSeparator();
871 runMenu_all = new QAction(tr("Run All"), this);
872 runMenu_all->setShortcut(QKeySequence("Ctrl+E"));
873 connect(runMenu_all, &QAction::triggered, this, &MainWindow::runAll);
874 runMenu->addAction(runMenu_all);
875 runMenu->addSeparator();
876 runMenu_copyCommand = new QAction(tr("Edit Command"), this);
877 runMenu_copyCommand->setShortcut(QKeySequence("Ctrl+Shift+C"));
878 connect(runMenu_copyCommand, &QAction::triggered, this, &MainWindow::copyCommand);
879 runMenu->addAction(runMenu_copyCommand);
880 runMenu->addSeparator();
881 QAction *runMenu_clearLog = new QAction(tr("Clear Log"), this);
882 runMenu_clearLog->setShortcut(QKeySequence("Ctrl+L"));
883 connect(runMenu_clearLog, &QAction::triggered, this, [this]() { bottomTextBox->clear(); });
884 runMenu->addAction(runMenu_clearLog);
885 play_repeat = new QAction(tr("Repeat"), this);
886 play_repeat->setShortcut(QKeySequence("Ctrl+R"));
887 play_repeat->setCheckable(true);
888 play_repeat->setChecked(false);
889 connect(play_repeat, &QAction::toggled, this, [this](bool) { publishRepeatStateToRunningProcess(); });
890 playbackMenu->addAction(play_repeat);
891 normalizedTimeAction = new QAction(tr("Normalized Time"), this);
892 normalizedTimeAction->setShortcut(QKeySequence("Ctrl+Alt+N"));
893 normalizedTimeAction->setCheckable(true);
894 normalizedTimeAction->setChecked(false);
895 normalizedTimeAction->setToolTip(tr("Advance shader time by a fixed amount per output frame."));
896 connect(normalizedTimeAction, &QAction::toggled, this, [this](bool checked) {
897 normalized_time = checked;
898 QSettings settings("LostSideDead", "acmx2");
899 settings.setValue("interface/normalized_time", checked);
901 });
903 play_stop = new QAction(tr("Stop"), this);
904 play_stop->setShortcut(QKeySequence("Shift+F5"));
905 play_stop->setEnabled(false);
906 connect(play_stop, &QAction::triggered, this, [=]() {
907 if (process->state() == QProcess::Running) {
908 process->terminate();
909 }
910 if (hdr10Process && hdr10Process->state() == QProcess::Running) {
911 hdr10Process->terminate();
912 }
913 });
914 playbackMenu->addAction(play_stop);
915 playbackMenu->addSeparator();
916 shaderPassAction = new QAction(tr("Multi-Pass Shader Settings..."), this);
917 shaderPassAction->setShortcut(QKeySequence("Ctrl+Alt+M"));
918 connect(shaderPassAction, &QAction::triggered, this, &MainWindow::menuShaderPassSettings);
919 playbackMenu->addAction(shaderPassAction);
920 playbackMenu->addSeparator();
921 playlistAction = new QAction(tr("Shader Playlist Settings..."), this);
922 playlistAction->setShortcut(QKeySequence("Ctrl+Alt+P"));
923 connect(playlistAction, &QAction::triggered, this, &MainWindow::menuPlaylistSettings);
924 playbackMenu->addAction(playlistAction);
925 playbackMenu->addSeparator();
926 buildCacheAction = new QAction(tr("Rebuild Shader Cache"), this);
927 buildCacheAction->setShortcut(QKeySequence("Ctrl+Alt+B"));
928 connect(buildCacheAction, &QAction::triggered, this, &MainWindow::menuBuildShaderCache);
929 playbackMenu->addAction(buildCacheAction);
930 fixBuildAction = new QAction(tr("Fix Build"), this);
931 fixBuildAction->setShortcut(QKeySequence("Ctrl+Alt+F"));
932 fixBuildAction->setToolTip(tr("Build ACMXVK while omitting shaders that fail to compile."));
933 connect(fixBuildAction, &QAction::triggered, this, &MainWindow::menuFixBuild);
934 playbackMenu->addAction(fixBuildAction);
935 cleanShaderCacheAction = new QAction(tr("Clean Shader Cache"), this);
936 cleanShaderCacheAction->setShortcut(QKeySequence("Ctrl+Alt+C"));
937 connect(cleanShaderCacheAction, &QAction::triggered, this, &MainWindow::menuCleanShaderCache);
939#ifdef Q_OS_MACOS
940 // macOS does not support the persistent binary shader cache.
941 buildCacheAction->setVisible(false);
942 buildCacheAction->setEnabled(false);
943 fixBuildAction->setVisible(false);
944 fixBuildAction->setEnabled(false);
945 cleanShaderCacheAction->setVisible(false);
946 cleanShaderCacheAction->setEnabled(false);
947#endif
948
949 removeBrokenAction = new QAction(tr("Remove Broken"), this);
950 removeBrokenAction->setShortcut(QKeySequence("Ctrl+Alt+R"));
951 connect(removeBrokenAction, &QAction::triggered, this, &MainWindow::menuRemoveBroken);
953
954 runFromCacheAction = new QAction(tr("Run from Cache"), this);
955 runFromCacheAction->setShortcut(QKeySequence("Ctrl+Alt+K"));
956 runFromCacheAction->setCheckable(true);
957#ifdef Q_OS_MACOS
958 use_shader_cache = false;
959 runFromCacheAction->setChecked(false);
960 runFromCacheAction->setEnabled(false);
961 runFromCacheAction->setToolTip(tr("Shader binary caching is not supported on macOS."));
962#else
963 runFromCacheAction->setChecked(true);
964#endif
965 connect(runFromCacheAction, &QAction::toggled, this, [this](bool checked) {
966 use_shader_cache = checked;
967 if (checked) {
968 Log("Shader cache enabled - will use cached shaders if available");
969 } else {
970 Log("Shader cache disabled - shaders will be recompiled each run");
971 }
972 });
974
975 playbackMenu->addSeparator();
976 midiSettingsAction = new QAction(tr("MIDI Settings..."), this);
977 midiSettingsAction->setShortcut(QKeySequence("Ctrl+Alt+I"));
978 connect(midiSettingsAction, &QAction::triggered, this, &MainWindow::menuMidiSettings);
980
981 playbackMenu->addSeparator();
982 watermarkAction = new QAction(tr("Watermark..."), this);
983 watermarkAction->setShortcut(QKeySequence("Ctrl+Alt+W"));
984 connect(watermarkAction, &QAction::triggered, this, &MainWindow::menuWatermarkSettings);
985 playbackMenu->addAction(watermarkAction);
986
987 displayFilterAction = new QAction(tr("Display"), this);
988 displayFilterAction->setShortcut(QKeySequence("Ctrl+Alt+D"));
989 displayFilterAction->setCheckable(true);
990 displayFilterAction->setChecked(false);
991 connect(displayFilterAction, &QAction::toggled, this, &MainWindow::menuToggleDisplayFilter);
993
994 listMenu_new = new QAction(tr("New Shader Library"), this);
995 listMenu_new->setShortcut(QKeySequence("Ctrl+Shift+N"));
996 connect(listMenu_new, &QAction::triggered, this, &MainWindow::newList);
997 listMenu->addAction(listMenu_new);
998 libraryBuilderAction = new QAction(tr("Shader Library Builder..."), this);
999 libraryBuilderAction->setShortcut(QKeySequence("Ctrl+Shift+B"));
1000 connect(libraryBuilderAction, &QAction::triggered, this, &MainWindow::menuLibraryBuilder);
1001 listMenu->addAction(libraryBuilderAction);
1002 listMenu_shader = new QAction(tr("New Shader File..."), this);
1003 listMenu_shader->setShortcut(QKeySequence::New);
1004 connect(listMenu_shader, &QAction::triggered, this, &MainWindow::newShader);
1005 listMenu->addAction(listMenu_shader);
1006 customUniformsAction = new QAction(tr("Custom Uniforms..."), this);
1007 customUniformsAction->setShortcut(QKeySequence("Ctrl+U"));
1008 connect(customUniformsAction, &QAction::triggered, this, &MainWindow::menuCustomUniforms);
1009 listMenu->addAction(customUniformsAction);
1010 listMenu->addSeparator();
1011 listMenu_remove = new QAction(tr("Remove Shader"), this);
1012 listMenu_remove->setShortcut(QKeySequence::Delete);
1013 connect(listMenu_remove, &QAction::triggered, this, &MainWindow::menuRemove);
1014 listMenu->addAction(listMenu_remove);
1015 listMenu_set_current = new QAction(tr("Set Current Shader"), this);
1016 listMenu_set_current->setShortcut(QKeySequence("Ctrl+Return"));
1017 listMenu_set_current->setEnabled(false);
1018 connect(listMenu_set_current, &QAction::triggered, this, &MainWindow::menuSetCurrentShader);
1019 listMenu->addAction(listMenu_set_current);
1020 listMenu->addSeparator();
1021 listMenu_up = new QAction(tr("Shift Shader Up"), this);
1022 listMenu_up->setShortcut(QKeySequence("Alt+Up"));
1023 connect(listMenu_up, &QAction::triggered, this, &MainWindow::menuUp);
1024 listMenu->addAction(listMenu_up);
1025 listMenu_down = new QAction(tr("Shift Shader Down"), this);
1026 listMenu_down->setShortcut(QKeySequence("Alt+Down"));
1027 connect(listMenu_down, &QAction::triggered, this, &MainWindow::menuDown);
1028 listMenu->addAction(listMenu_down);
1029 listMenu_shuffle = new QAction(tr("Shuffle Shaders"), this);
1030 listMenu_shuffle->setShortcut(QKeySequence("Ctrl+Shift+H"));
1031 connect(listMenu_shuffle, &QAction::triggered, this, &MainWindow::menuShuffle);
1032 listMenu->addAction(listMenu_shuffle);
1033
1034 listMenu_sort = new QAction(tr("Sort Shaders"), this);
1035 listMenu_sort->setShortcut(QKeySequence("Ctrl+Shift+S"));
1036 connect(listMenu_sort, &QAction::triggered, this, &MainWindow::menuSort);
1037 listMenu->addAction(listMenu_sort);
1038 listMenu->addSeparator();
1039 listMenu_search = new QAction(tr("Search Shaders"), this);
1040 listMenu_search->setShortcut(QKeySequence("Ctrl+F"));
1041 connect(listMenu_search, &QAction::triggered, this, &MainWindow::menuSearch);
1042 listMenu->addAction(listMenu_search);
1043 listMenu_findNext = new QAction(tr("Find Next"), this);
1044 listMenu_findNext->setShortcut(QKeySequence("F3"));
1045 connect(listMenu_findNext, &QAction::triggered, this, &MainWindow::menuFindNext);
1046 listMenu->addAction(listMenu_findNext);
1047 listMenu_findInFiles = new QAction(tr("Find in Files..."), this);
1048 listMenu_findInFiles->setShortcut(QKeySequence("Ctrl+Shift+F"));
1049 connect(listMenu_findInFiles, &QAction::triggered, this, [this]() {
1050 if (shader_path.isEmpty() || !QDir(shader_path).exists()) {
1051 QMessageBox::information(this, tr("Find in Files"), tr("Load a shader library before searching its files."));
1052 return;
1053 }
1054
1055 auto *dialog = new FindShaderDialog(shader_path, this);
1056 connect(dialog, &FindShaderDialog::resultActivated, this, [this](const QString &filePath, int lineNumber, int columnNumber, int matchLength) { openShaderEditor(filePath, lineNumber, columnNumber, matchLength); });
1057 dialog->show();
1058 dialog->raise();
1059 dialog->activateWindow();
1060 });
1061 listMenu->addAction(listMenu_findInFiles);
1062 helpMenu_uniformReference = new QAction(tr("Built-in Uniform Reference..."), this);
1063 helpMenu_uniformReference->setShortcut(QKeySequence::HelpContents);
1064 connect(helpMenu_uniformReference, &QAction::triggered, this, &MainWindow::menuUniformReference);
1066 helpMenu->addSeparator();
1067
1068 helpMenu_about = new QAction("About", this);
1069 helpMenu_about->setShortcut(QKeySequence("Shift+F1"));
1070
1071 connect(helpMenu_about, &QAction::triggered, this, [=]() {
1072 QMessageBox box(this);
1073 box.setWindowTitle("About ACMX2");
1074 box.setWindowIcon(QIcon(":/win-icon.png"));
1075 const QString info = QStringLiteral("<p><b>ACMX %1</b><br>"
1076 "(C) 2026 %2 Software<br>"
1077 "<a href=\"https://lostsidedead.biz\">"
1078 "http://lostsidedead.biz</a><br>"
1079 "This software is dedicated to all that have "
1080 "experienced mental health issues.</p>")
1081 .arg(QStringLiteral(VERSION_INFO), QStringLiteral(VERSION_AUTHOR));
1082 box.setTextFormat(Qt::RichText);
1083 box.setTextInteractionFlags(Qt::TextBrowserInteraction);
1084 box.setText(info);
1085 for (QLabel *label : box.findChildren<QLabel *>())
1086 label->setOpenExternalLinks(true);
1087 QPixmap bigIcon(":/win-icon.png");
1088 if (!bigIcon.isNull()) {
1089 QPixmap resizedIcon = bigIcon.scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
1090 box.setIconPixmap(resizedIcon);
1091 }
1092 box.exec();
1093 });
1094 helpMenu->addAction(helpMenu_about);
1099 const QString shaderName = currentShaderName();
1100 if (!shaderName.isEmpty())
1101 publishShaderReloadToRunningProcess(QDir(shader_path).filePath(shaderName));
1102 });
1103 list_view = new QTreeWidget(this);
1104 list_view->setColumnCount(5);
1105 list_view->setHeaderLabels({tr("#"), tr("Name"), tr("Last Modified"), tr("Compile Health"), tr("Type")});
1106 list_view->setRootIsDecorated(false);
1107 list_view->setUniformRowHeights(true);
1108 list_view->setAlternatingRowColors(false);
1109 list_view->setSelectionMode(QAbstractItemView::SingleSelection);
1110 list_view->setSelectionBehavior(QAbstractItemView::SelectRows);
1111 list_view->setContextMenuPolicy(Qt::CustomContextMenu);
1112 list_view->setSortingEnabled(false);
1113 list_view->setAllColumnsShowFocus(true);
1114 list_view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
1115 list_view->header()->setSectionResizeMode(1, QHeaderView::Stretch);
1116 list_view->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
1117 list_view->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
1118 list_view->header()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
1119#ifdef Q_OS_MACOS
1120 // macOS does not support the persistent shader cache; hide the column.
1121 list_view->setColumnHidden(3, true);
1122#endif
1123 list_view->setToolTip(tr("Right click while running to change the active shader."));
1124 bottomTextBox = new QTextEdit(this);
1125 bottomTextBox->setHtml("<b style='color:red;'>ACMX</b> - Interface: Loaded.");
1126 bottomTextBox->setReadOnly(true);
1127 connect(list_view, &QTreeWidget::doubleClicked, this, &MainWindow::listClicked);
1128 connect(list_view, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
1129 if (!list_view)
1130 return;
1131 if (QTreeWidgetItem *item = list_view->itemAt(pos)) {
1132 list_view->setCurrentItem(item);
1134 if (process && process->state() == QProcess::Running) {
1135 return;
1136 }
1137 }
1138 if (listMenu) {
1139 listMenu->exec(list_view->viewport()->mapToGlobal(pos));
1140 }
1141 });
1142 QWidget *centralWidget = new QWidget(this);
1143 QVBoxLayout *layout = new QVBoxLayout(centralWidget);
1144 layout->addWidget(list_view, 3);
1145 layout->addWidget(bottomTextBox, 1);
1146 centralWidget->setLayout(layout);
1147 setCentralWidget(centralWidget);
1148 QSettings appSettings("LostSideDead");
1149 active_backend = acmx2::backend_from_id(appSettings.value("interface/backend", "acmx2").toString()).value_or(acmx2::Backend::Acmx2);
1153 baseAppStyleSheet = qApp->styleSheet();
1154 const QString legacyLibrary = active_backend == acmx2::Backend::Acmx2 ? appSettings.value("shaders", "").toString() : QString();
1155 QString path = appSettings.value(acmx2::backend_settings_key(active_backend, "library"), legacyLibrary).toString();
1156 path = path.trimmed();
1157 while (path.endsWith("/") || path.endsWith("\\")) {
1158 path.chop(1);
1159 }
1160 const QString legacyExecutable = active_backend == acmx2::Backend::Acmx2 ? appSettings.value("exePath", acmx2::default_backend_executable(acmx2::Backend::Acmx2)).toString() : acmx2::default_backend_executable(active_backend);
1161 executable_path = appSettings.value(acmx2::backend_settings_key(active_backend, "executable"), legacyExecutable).toString();
1162 prefix_path = appSettings.value("prefix_path", ".").toString();
1165 bool useCustomStyle = appSettings.value("useCustomStyle", false).toBool();
1166 styleSheetAction->setChecked(useCustomStyle);
1167 midi_enabled = appSettings.value("midiEnabled", false).toBool();
1168 midi_config_file = appSettings.value("midiConfigFile", "").toString();
1169 midi_device = appSettings.value("midiDevice", -1).toInt();
1170 watermark_enabled = appSettings.value("watermarkEnabled", false).toBool();
1171 watermark_text = appSettings.value("watermarkText", "").toString();
1172 watermark_r = appSettings.value("watermarkR", 255).toInt();
1173 watermark_g = appSettings.value("watermarkG", 0).toInt();
1174 watermark_b = appSettings.value("watermarkB", 150).toInt();
1175 display_filter_enabled = appSettings.value("displayFilter", false).toBool();
1176 autopilot_frames = appSettings.value("playlistAutopilotFrames", 4).toInt();
1177 if (autopilot_frames < 4) {
1178 autopilot_frames = 4;
1179 }
1180 autopilot_random = appSettings.value("playlistAutopilotRandom", false).toBool();
1181 if (displayFilterAction) {
1182 QSignalBlocker blocker(displayFilterAction);
1184 }
1186 if (!path.isEmpty()) {
1187 QFileInfo pathInfo(path);
1188 if (pathInfo.exists() && pathInfo.isDir() && acmx2::shader_manifest_exists(path)) {
1189 QString backendError;
1190 const std::optional<acmx2::Backend> libraryBackend = acmx2::shader_manifest_backend(path, backendError);
1191 if (!backendError.isEmpty()) {
1192 Log("Warning: Saved shader library backend metadata is invalid: " + backendError);
1193 } else if (libraryBackend && *libraryBackend != active_backend) {
1194 Log(tr("Warning: Saved shader library targets %1 while the "
1195 "active backend is %2: %3")
1196 .arg(acmx2::backend_name(*libraryBackend), acmx2::backend_name(active_backend), path));
1197 } else {
1198 shader_path = path;
1199 loadShaders(path);
1200 addRecentLibrary(path);
1201 Log("Successfully loaded saved shader path");
1202 }
1203 } else {
1204 QString errorMsg = "Warning: Saved shader path is invalid: " + path + " - ";
1205 if (!pathInfo.exists()) {
1206 errorMsg += "directory does not exist";
1207 } else if (!pathInfo.isDir()) {
1208 errorMsg += "path is not a directory";
1209 } else if (!acmx2::shader_manifest_exists(path)) {
1210 errorMsg += "library.json or index.txt not found in directory";
1211 }
1212 Log(errorMsg);
1213 }
1214 }
1216 const QString defaultCustomStyleSheet = acmx2::defaultCustomStyleSheet();
1217 customStyleSheet = appSettings.value("customStyleSheet", defaultCustomStyleSheet).toString();
1218
1219 applyCustomStyleSheet(useCustomStyle);
1220}
1221
1223 QSettings settings("LostSideDead", "acmx2");
1224
1225 const QString inputMode = settings.value("interface/input_mode", "camera").toString();
1226 const bool videoMode = inputMode == "video";
1227 const bool graphicsMode = inputMode == "graphic";
1228 const bool cameraMode = !videoMode && !graphicsMode;
1229
1230 camera_index = static_cast<unsigned int>(std::max(0, settings.value("interface/camera_device", 0).toInt()));
1231 camera_res = storedResolution(settings, "interface/camera_resolution", QSize(1280, 720), false);
1232 screen_res = storedResolution(settings, "interface/screen_resolution", QSize(0, 0), true);
1233
1234 output_fps = settings.value("interface/camera_fps", 30.0).toDouble();
1235 if (output_fps <= 0.0)
1236 output_fps = 30.0;
1237
1238 video_file = videoMode ? settings.value("interface/input_video", "").toString() : QString();
1239 graphics_file = graphicsMode ? settings.value("interface/graphics_file", "").toString() : QString();
1240
1241 const bool saveOutput = settings.value("interface/save_output", false).toBool();
1242 output_file = saveOutput ? settings.value("interface/output_video", "").toString() : QString();
1243 save_output_log = saveOutput && settings.value("interface/save_output_log", false).toBool();
1244 full_screen_value = settings.value("interface/fullscreen", false).toBool();
1245 copy_audio = videoMode && saveOutput && settings.value("interface/copy_audio", false).toBool();
1246
1247 cache_enabled = !graphicsMode && settings.value("interface/texture_cache", false).toBool();
1248 cache_delay = settings.value("interface/cache_delay", 1).toInt();
1249 cache_size = std::clamp(settings.value("interface/cache_size", 8).toInt(), 1, 64);
1250 use_yuv = cameraMode && settings.value("interface/use_yuv", false).toBool();
1251
1252 convert_to_hdr10 = videoMode && saveOutput && settings.value("interface/convert_to_hdr10", false).toBool();
1253 enable_3d = settings.value("interface/enable_3d", false).toBool();
1254 model_file = settings.value("interface/model_file", "cube.mxmod.z").toString();
1255 onnx_model_enabled = settings.value("interface/use_onnx_model", false).toBool();
1256 onnx_model = settings.value("interface/onnx_model_file", "").toString();
1257 deep_dream_enabled = settings.value("deep_dream/enabled", false).toBool();
1258 deep_dream_model = settings.value("deep_dream/model_file", QString()).toString();
1259 deep_dream_layer = settings.value("deep_dream/layer", "relu4_2").toString();
1260 deep_dream_iterations = std::clamp(settings.value("deep_dream/iterations", 1).toInt(), 1, 100);
1261 deep_dream_strength = std::clamp(settings.value("deep_dream/strength", 0.05).toDouble(), 0.0001, 10.0);
1262 deep_dream_feedback = std::clamp(settings.value("deep_dream/feedback", 0.9).toDouble(), 0.0, 0.99);
1263 deep_dream_zoom = std::clamp(settings.value("deep_dream/zoom", 1.01).toDouble(), 0.9, 1.1);
1264 deep_dream_rotation = std::clamp(settings.value("deep_dream/rotation", 0.1).toDouble(), -5.0, 5.0);
1265 deep_dream_maximum_dimension = settings.value("deep_dream/maximum_dimension", 512).toInt();
1268 }
1269 deep_dream_fp16 = settings.value("deep_dream/fp16", false).toBool();
1270 deep_dream_channel = std::clamp(settings.value("deep_dream/channel", -1).toInt(), -1, 65535);
1271 deep_dream_octaves = std::clamp(settings.value("deep_dream/octaves", 1).toInt(), 1, 8);
1272 deep_dream_octave_scale = std::clamp(settings.value("deep_dream/octave_scale", 1.4).toDouble(), 1.1, 3.0);
1273 deep_dream_jitter = std::clamp(settings.value("deep_dream/jitter", 0).toInt(), 0, 64);
1274 deep_dream_smoothing = std::clamp(settings.value("deep_dream/smoothing", 0).toInt(), 0, 16);
1275 deep_dream_gpu_filter_first = settings.value("deep_dream/gpu_filter_first", false).toBool();
1276 deep_dream_original = settings.value("deep_dream/deep_original", false).toBool();
1277 if (deep_dream_original) {
1278 deep_dream_feedback = 0.0;
1279 deep_dream_zoom = 1.0;
1280 deep_dream_rotation = 0.0;
1281 }
1282 stable_diffusion_enabled = settings.value("stable_diffusion/enabled", false).toBool();
1283 stable_diffusion_model = settings.value("stable_diffusion/model_file").toString();
1284 stable_diffusion_lora_files = settings.value("stable_diffusion/lora_files").toStringList();
1285 const QStringList stable_diffusion_lora_multiplier_values = settings.value("stable_diffusion/lora_multipliers").toStringList();
1286 for (int index = 0; index < stable_diffusion_lora_files.size(); ++index) {
1287 bool multiplier_ok = false;
1288 const double multiplier = index < stable_diffusion_lora_multiplier_values.size() ? stable_diffusion_lora_multiplier_values.at(index).toDouble(&multiplier_ok) : 1.0;
1289 stable_diffusion_lora_multipliers.append(multiplier_ok ? std::clamp(multiplier, -10.0, 10.0) : 1.0);
1290 }
1291 stable_diffusion_upscale_only = settings.value("stable_diffusion/upscale_only", false).toBool();
1292 if (stable_diffusion_upscale_only || settings.value("stable_diffusion/server_upscale", false).toBool()) {
1293 stable_diffusion_upscale_model = settings.value("stable_diffusion/upscale_model_file").toString();
1294 }
1295 stable_diffusion_prompt = settings.value("stable_diffusion/prompt").toString();
1296 stable_diffusion_negative_prompt = settings.value("stable_diffusion/negative_prompt").toString();
1297 stable_diffusion_server = settings.value("stable_diffusion/server", default_stable_diffusion_server()).toString();
1298 stable_diffusion_server_arguments = settings.value("stable_diffusion/server_arguments").toString();
1299 stable_diffusion_server_port = std::clamp(settings.value("stable_diffusion/port", 1234).toInt(), 1024, 65535);
1300 stable_diffusion_width = std::clamp(settings.value("stable_diffusion/width", 576).toInt(), 64, 2048);
1301 stable_diffusion_height = std::clamp(settings.value("stable_diffusion/height", 320).toInt(), 64, 2048);
1302 stable_diffusion_upscale_working_width = std::clamp(settings.value("stable_diffusion/upscale_working_width", 0).toInt(), 0, 4096);
1303 stable_diffusion_upscale_working_height = std::clamp(settings.value("stable_diffusion/upscale_working_height", 0).toInt(), 0, 4096);
1304 stable_diffusion_steps = std::clamp(settings.value("stable_diffusion/steps", 12).toInt(), 1, 150);
1305 stable_diffusion_strength = std::clamp(settings.value("stable_diffusion/strength", 0.35).toDouble(), 0.01, 1.0);
1306 stable_diffusion_cfg_scale = std::clamp(settings.value("stable_diffusion/cfg_scale", 5.0).toDouble(), 0.0, 50.0);
1307 stable_diffusion_seed = settings.value("stable_diffusion/seed", 1234).toInt();
1308 stable_diffusion_sampler = settings.value("stable_diffusion/sampler", "euler_a").toString();
1309 stable_diffusion_scheduler = settings.value("stable_diffusion/scheduler", "discrete").toString();
1310 stable_diffusion_upscale = settings.value("stable_diffusion/upscale", false).toBool() && !stable_diffusion_upscale_only;
1311 cuda_device = settings.value("interface/cuda_device", 0).toInt();
1312 time_speed = settings.value("interface/time_speed", 1.0).toFloat();
1313 normalized_time = settings.value("interface/normalized_time", false).toBool();
1315 QSignalBlocker blocker(normalizedTimeAction);
1317 }
1318 duration_limit_enabled = settings.value("interface/duration_enabled", false).toBool();
1319 max_duration = settings.value("interface/duration_seconds", 60.0).toDouble();
1320 max_size_limit_enabled = settings.value("interface/max_size_enabled", false).toBool();
1321 max_size_mb = settings.value("interface/max_size_mb", 500.0).toDouble();
1322 cross_fade_duration = settings.value("interface/crossfade", 0.5).toFloat();
1323 flip_enabled = settings.value("interface/flip", false).toBool();
1324 rotate_enabled = settings.value("interface/rotate", false).toBool();
1325 rotation_mode = settings.value("interface/rotation_mode", "clockwise").toString();
1326 png_output = active_backend == acmx2::Backend::Acmxvk && settings.value("interface/write_png", false).toBool();
1327 png_output_directory = settings.value("interface/png_output_directory", "").toString();
1328 png_level = std::clamp(settings.value("interface/png_level", 6).toInt(), 1, 9);
1329 generate_enabled = settings.value("interface/generate_enabled", false).toBool();
1330 generate_interval = settings.value("interface/generate_interval", 30).toInt();
1331
1332 encode_preset = settings.value("recording/preset", "medium").toString();
1333 encode_tune = settings.value("recording/tune", "").toString();
1334 encode_crf = settings.value("recording/crf", 18).toInt();
1335 encode_rate_control = settings.value("recording/rate_control", "quality").toString();
1336 encode_bitrate = settings.value("recording/bitrate", "10M").toString();
1337 encode_codec = settings.value("recording/codec", "auto").toString();
1338 encode_parameters = settings.value("recording/parameters", "").toString();
1339 encode_realtime = settings.value("recording/realtime", false).toBool();
1340 encode_no_drop = !cameraMode && settings.value("recording/no_drop", false).toBool();
1341 encode_constant_frame_rate = settings.value("recording/constant_frame_rate", false).toBool();
1342 encode_fill_pts_gaps = settings.value("recording/fill_pts_gaps", false).toBool();
1343 maximize_fps = settings.value("interface/acmxvk_maximize_fps", false).toBool();
1344 use_source_fps = settings.value("interface/acmxvk_use_source_fps", false).toBool();
1345 use_source_audio = use_source_fps && settings.value("interface/acmxvk_use_source_audio", false).toBool();
1346 extra_arguments = settings.value("interface/extra_arguments", QString()).toString();
1347}
1348
1349void MainWindow::applyMainViewStyles(bool customStyleEnabled) {
1350 if (list_view) {
1351 QFont listFont("Courier New");
1352 listFont.setStyleHint(QFont::Monospace);
1353 listFont.setPointSize(12);
1354 list_view->setFont(listFont);
1355
1356 if (customStyleEnabled) {
1357 list_view->setStyleSheet("");
1358 } else {
1359 list_view->setStyleSheet("QTreeWidget { background-color: black; color: white; font-size: 13px;"
1360 " font-family: 'Courier New', Courier, monospace; }"
1361 "QHeaderView::section { background-color: #110000; color: lime;"
1362 " font-family: 'Courier New', Courier, monospace; padding: 4px;"
1363 " border: 1px solid #330000; }");
1364 }
1365 }
1366
1367 if (bottomTextBox) {
1368 QFont logFont("Courier New");
1369 logFont.setStyleHint(QFont::Monospace);
1370 logFont.setPointSize(11);
1371 bottomTextBox->setFont(logFont);
1372
1373 if (customStyleEnabled) {
1374 bottomTextBox->setStyleSheet("");
1375 } else {
1376 bottomTextBox->setStyleSheet("QTextEdit { background-color: black; color: lime; font-size: 13px;"
1377 " font-family: 'Courier New', Courier, monospace; }");
1378 }
1379 }
1380}
1381
1383 QSettings appSettings("LostSideDead");
1384 appSettings.setValue("useCustomStyle", enable);
1385
1386 if (baseAppStyleSheet.isEmpty()) {
1387 baseAppStyleSheet = qApp->styleSheet();
1388 }
1389
1390 if (enable) {
1391 qApp->setStyleSheet(customStyleSheet);
1392 } else {
1393 qApp->setStyleSheet(baseAppStyleSheet);
1394 }
1395
1396 // Keep this window clean so it follows the global app style consistently.
1397 setStyleSheet("");
1398 applyMainViewStyles(enable);
1399}
1400
1402 QSettings appSettings("LostSideDead");
1403 const bool currentlyEnabled = appSettings.value("useCustomStyle", false).toBool();
1404 const QString lastPresetName = appSettings.value("customStylePreset", "Current Style").toString();
1405
1406 auto makePalette = [](const char *winBg, const char *winFg, const char *accent, const char *fieldBg, const char *fieldFg, const char *fieldBorder, const char *btnBg, const char *btnHover, const char *btnFg, const char *menuBg, const char *menuFg, const char *menuSelBg, const char *menuSelFg, const char *selBg, const char *border) {
1408 p.windowBg = winBg;
1409 p.windowFg = winFg;
1410 p.accent = accent;
1411 p.fieldBg = fieldBg;
1412 p.fieldFg = fieldFg;
1413 p.fieldBorder = fieldBorder;
1414 p.buttonBg = btnBg;
1415 p.buttonHover = btnHover;
1416 p.buttonFg = btnFg;
1417 p.menuBg = menuBg;
1418 p.menuFg = menuFg;
1419 p.menuSelBg = menuSelBg;
1420 p.menuSelFg = menuSelFg;
1421 p.selectionBg = selBg;
1422 p.border = border;
1423 return acmx2::buildStyleSheet(p);
1424 };
1425
1426 const std::array<QPair<QString, QString>, 26> presetStyles = {{{"Current Style", customStyleSheet},
1427 {"Light: Blue & White", makePalette("#f6fbff", "#143a5c", "#2d7cc4", "#ffffff", "#123b61", "#9cc6ea", "#2d7cc4", "#2368a6", "#ffffff", "#eaf5ff", "#143a5c", "#cfe6ff", "#0b2e4d", "#bcdcff", "1px solid #9cc6ea")},
1428 {"Light: Slate", makePalette("#f5f7fa", "#1f2a37", "#4b5563", "#ffffff", "#1f2937", "#b6c3d4", "#4b5563", "#374151", "#ffffff", "#e8edf4", "#1f2a37", "#d2dbe7", "#111827", "#cdd5e0", "1px solid #b6c3d4")},
1429 {"Light: White & Red", makePalette("#fffdfd", "#5b1515", "#d63b3b", "#ffffff", "#5a1a1a", "#e8bcbc", "#d63b3b", "#bc2f2f", "#ffffff", "#fff4f4", "#5b1515", "#ffdede", "#4b0f0f", "#ffd1d1", "1px solid #e8bcbc")},
1430 {"Light: White & Green", makePalette("#fcfffc", "#164529", "#2e9d57", "#ffffff", "#1a4f2f", "#b8dfc7", "#2e9d57", "#25824a", "#ffffff", "#f1fbf4", "#164529", "#d6f3df", "#11361f", "#c9eecf", "1px solid #b8dfc7")},
1431 {"Light: White & Blue", makePalette("#fcfdff", "#16395f", "#2f6ed7", "#ffffff", "#1b446f", "#b7d0f0", "#2f6ed7", "#285db7", "#ffffff", "#f1f6ff", "#16395f", "#d9e8ff", "#102b49", "#cddfff", "1px solid #b7d0f0")},
1432 {"Light: White & Cyan", makePalette("#fbfeff", "#12404a", "#1ea9bf", "#ffffff", "#14505d", "#b8e2ea", "#1ea9bf", "#198da0", "#ffffff", "#effbfe", "#12404a", "#d5f3f8", "#0e3138", "#c7edf4", "1px solid #b8e2ea")},
1433 {"Light: White & Amber", makePalette("#fffefb", "#5a3a12", "#d18b1f", "#ffffff", "#644317", "#ead7b6", "#d18b1f", "#b37518", "#ffffff", "#fff9ed", "#5a3a12", "#ffebcb", "#4a2f0f", "#ffe2b5", "1px solid #ead7b6")},
1434 {"Dark: Crimson", makePalette("#0f0608", "#ff637d", "#a02949", "#1b0b10", "#ff8fa3", "#7f2036", "#6f1630", "#8a1f3d", "#ffdfe6", "#16090d", "#ff637d", "#52111f", "#ffd5dc", "#52111f", "2px solid #a02949")},
1435 {"Dark: Emerald", makePalette("#06110c", "#7af7c2", "#2c8e68", "#0d1e16", "#95ffd0", "#2c8e68", "#1c6a4d", "#258961", "#dcfff2", "#08160f", "#7af7c2", "#12402d", "#d9fff0", "#12402d", "2px solid #2c8e68")},
1436 {"Dark: Indigo", makePalette("#070713", "#c6c8ff", "#5362ba", "#121634", "#d8daff", "#4956a5", "#36439a", "#4453b4", "#eef0ff", "#0d1022", "#c6c8ff", "#232a5a", "#eef0ff", "#232a5a", "2px solid #5362ba")},
1437 {"Dark: Black & Red", makePalette("#050505", "#ff4d4d", "#d90000", "#120808", "#ff7b7b", "#b50000", "#2a0c0c", "#3a1010", "#ffd6d6", "#0b0707", "#ff5a5a", "#6b1111", "#ffe9e9", "#5a0c0c", "2px solid #d90000")},
1438 {"Dark: Black & Green", makePalette("#040704", "#6dfb88", "#22b44a", "#0a140b", "#a8ffbe", "#1d9a3e", "#12331b", "#164425", "#e1ffe8", "#08100a", "#74ff95", "#12331b", "#e7ffed", "#10381d", "2px solid #22b44a")},
1439 {"Dark: Black & Blue", makePalette("#04060a", "#81b9ff", "#2f6ed7", "#0a1222", "#b4d4ff", "#2a5eb7", "#132749", "#1a3260", "#e7f1ff", "#070d1a", "#8cc0ff", "#1a3260", "#eef5ff", "#17335f", "2px solid #2f6ed7")},
1440 {"Dark: Black & Cyan", makePalette("#030809", "#7defff", "#1ba8c3", "#09161a", "#b8f7ff", "#1990a7", "#10323a", "#14414b", "#e7fbff", "#071015", "#89f3ff", "#0f3943", "#e8fcff", "#0f3943", "2px solid #1ba8c3")},
1441 {"Dark: Black & Amber", makePalette("#090704", "#ffd77a", "#d88c1d", "#1a1308", "#ffe7b4", "#bf7a19", "#3d2810", "#523618", "#fff3db", "#130e07", "#ffdf8a", "#5a3a16", "#fff4df", "#5a3a16", "2px solid #d88c1d")},
1442 {"Light: Lavender Mist", makePalette("#f8f6ff", "#302653", "#7157c8", "#ffffff", "#34295b", "#c9bdea", "#7157c8", "#5d45ae", "#ffffff", "#eee9ff", "#302653", "#ded5ff", "#241a48", "#d9d0ff", "1px solid #c9bdea")},
1443 {"Light: Rose Quartz", makePalette("#fff8fa", "#532535", "#c25578", "#ffffff", "#5b293c", "#e8c1cf", "#c25578", "#a94465", "#ffffff", "#fff0f4", "#532535", "#f6d7e1", "#411a28", "#f1ccd8", "1px solid #e8c1cf")},
1444 {"Light: Sandstone", makePalette("#fbf7ef", "#493728", "#a66a3f", "#fffdf8", "#4f3929", "#d9c3aa", "#a66a3f", "#895431", "#ffffff", "#f3eadc", "#493728", "#ead8c1", "#35251a", "#e5d1b7", "1px solid #d9c3aa")},
1445 {"Light: Mint & Navy", makePalette("#f3fbf8", "#173a3c", "#2b8c7f", "#ffffff", "#173a3c", "#addbd2", "#1d5962", "#287681", "#ffffff", "#e5f6f1", "#173a3c", "#c8eee5", "#102f34", "#bde5dc", "1px solid #addbd2")},
1446 {"Light: High Contrast", makePalette("#ffffff", "#111111", "#005fcc", "#ffffff", "#000000", "#4d4d4d", "#111111", "#005fcc", "#ffffff", "#f0f0f0", "#000000", "#005fcc", "#ffffff", "#9dccff", "2px solid #111111")},
1447 {"Dark: Cyberpunk Neon", makePalette("#070513", "#f3e7ff", "#ff2bd6", "#100c24", "#5ffbf1", "#6e4cff", "#2a145c", "#ff2bd6", "#ffffff", "#0c081c", "#5ffbf1", "#381b72", "#ffffff", "#381b72", "2px solid #ff2bd6")},
1448 {"Dark: Dracula", makePalette("#282a36", "#f8f8f2", "#bd93f9", "#21222c", "#f8f8f2", "#6272a4", "#44475a", "#6272a4", "#f8f8f2", "#21222c", "#f8f8f2", "#44475a", "#f8f8f2", "#44475a", "1px solid #6272a4")},
1449 {"Dark: Nord Frost", makePalette("#2e3440", "#eceff4", "#88c0d0", "#3b4252", "#eceff4", "#4c566a", "#4c566a", "#5e81ac", "#eceff4", "#242933", "#d8dee9", "#434c5e", "#eceff4", "#434c5e", "1px solid #88c0d0")},
1450 {"Dark: Solarized", makePalette("#002b36", "#93a1a1", "#b58900", "#073642", "#eee8d5", "#586e75", "#07576b", "#268bd2", "#fdf6e3", "#00242d", "#93a1a1", "#07576b", "#fdf6e3", "#07576b", "1px solid #586e75")},
1451 {"Dark: Graphite Orange", makePalette("#171717", "#f2f2f2", "#ff8a3d", "#242424", "#f7f7f7", "#5f5f5f", "#3a3a3a", "#ff8a3d", "#ffffff", "#202020", "#f2f2f2", "#59311c", "#ffffff", "#59311c", "2px solid #ff8a3d")}}};
1452
1453 if (styleSheetAction) {
1454 QSignalBlocker blocker(styleSheetAction);
1455 styleSheetAction->setChecked(currentlyEnabled);
1456 }
1457
1458 QDialog dialog(this);
1459 dialog.setWindowTitle(tr("Custom Style Editor"));
1460 dialog.resize(900, 640);
1461 // Keep the editor dialog on the application stylesheet so Apply updates it live.
1462 dialog.setStyleSheet("");
1463
1464 auto *layout = new QVBoxLayout(&dialog);
1465 auto *topRow = new QHBoxLayout();
1466 auto *enableCheck = new QCheckBox(tr("Use custom style"), &dialog);
1467 enableCheck->setChecked(currentlyEnabled);
1468 auto *presetLabel = new QLabel(tr("Preset:"), &dialog);
1469 auto *presetCombo = new QComboBox(&dialog);
1470 for (const auto &preset : presetStyles) {
1471 presetCombo->addItem(preset.first);
1472 }
1473 int presetIndex = 0;
1474 for (int i = 0; i < static_cast<int>(presetStyles.size()); ++i) {
1475 if (presetStyles[static_cast<std::size_t>(i)].first == lastPresetName) {
1476 presetIndex = i;
1477 break;
1478 }
1479 }
1480 presetCombo->setCurrentIndex(presetIndex);
1481
1482 auto *editor = new QPlainTextEdit(&dialog);
1483 editor->setPlainText(customStyleSheet);
1484 editor->setLineWrapMode(QPlainTextEdit::NoWrap);
1485 editor->setPlaceholderText(tr("Enter a Qt stylesheet (QSS) for ACMX2 interface..."));
1486 {
1487 QFont qssFont("Courier New");
1488 qssFont.setStyleHint(QFont::Monospace);
1489 qssFont.setPointSize(10);
1490 editor->setFont(qssFont);
1491 }
1492
1493 auto *buttonBox = new QDialogButtonBox(&dialog);
1494 auto *applyButton = buttonBox->addButton(tr("Apply"), QDialogButtonBox::ApplyRole);
1495 auto *saveButton = buttonBox->addButton(tr("Save"), QDialogButtonBox::ActionRole);
1496 auto *closeButton = buttonBox->addButton(QDialogButtonBox::Close);
1497
1498 topRow->addWidget(enableCheck);
1499 topRow->addSpacing(12);
1500 topRow->addWidget(presetLabel);
1501 topRow->addWidget(presetCombo, 1);
1502 layout->addLayout(topRow);
1503 layout->addWidget(editor, 1);
1504 layout->addWidget(buttonBox);
1505
1506 connect(presetCombo, &QComboBox::currentTextChanged, &dialog, [editor, &presetStyles, &appSettings](const QString &name) {
1507 for (const auto &preset : presetStyles) {
1508 if (preset.first == name) {
1509 editor->setPlainText(preset.second);
1510 appSettings.setValue("customStylePreset", name);
1511 break;
1512 }
1513 }
1514 });
1515
1516 auto applyEditorStyle = [this, &dialog, enableCheck, editor, presetCombo]() {
1517 customStyleSheet = editor->toPlainText();
1518 QSettings styleSettings("LostSideDead");
1519 styleSettings.setValue("customStyleSheet", customStyleSheet);
1520 styleSettings.setValue("customStylePreset", presetCombo->currentText());
1521 styleSettings.setValue("useCustomStyle", enableCheck->isChecked());
1522 applyCustomStyleSheet(enableCheck->isChecked());
1523 // Ensure no local override remains so the dialog always follows qApp style.
1524 dialog.setStyleSheet("");
1525 if (styleSheetAction) {
1526 QSignalBlocker blocker(styleSheetAction);
1527 styleSheetAction->setChecked(enableCheck->isChecked());
1528 }
1529 };
1530
1531 connect(applyButton, &QPushButton::clicked, &dialog, applyEditorStyle);
1532 connect(saveButton, &QPushButton::clicked, &dialog, applyEditorStyle);
1533 connect(closeButton, &QPushButton::clicked, &dialog, &QDialog::accept);
1534
1535 dialog.exec();
1536}
1537
1539 LibraryWindow library(active_backend, this);
1540
1541 if (library.exec() == QDialog::Accepted) {
1542 loadLibraryPath(library.getShaderPath());
1543 }
1544}
1545
1547 if (libraryBuilderDialog && libraryBuilderDialog->selectedBackend() != active_backend) {
1548 libraryBuilderDialog->close();
1549 libraryBuilderDialog = nullptr;
1550 }
1552 libraryBuilderDialog->show();
1553 libraryBuilderDialog->raise();
1554 libraryBuilderDialog->activateWindow();
1555 return;
1556 }
1557
1559 libraryBuilderDialog->setAttribute(Qt::WA_DeleteOnClose);
1560 connect(libraryBuilderDialog, &LibraryBuilderDialog::libraryExported, this, [this](const QString &directory) {
1561 if (loadLibraryPath(directory))
1562 Log(tr("Loaded exported shader library: %1").arg(shader_path));
1563 });
1564 libraryBuilderDialog->show();
1565 libraryBuilderDialog->raise();
1566 libraryBuilderDialog->activateWindow();
1567}
1568
1570 bool ok;
1571 QString searchText = QInputDialog::getText(this, tr("Search Shaders"), tr("Enter shader name to search:"), QLineEdit::Normal, lastSearchText, &ok);
1572
1573 if (!ok || searchText.isEmpty()) {
1574 return;
1575 }
1576
1577 lastSearchText = searchText;
1578 lastFoundIndex = -1;
1579 if (items.isEmpty()) {
1580 QMessageBox::information(this, tr("Search Shaders"), tr("No shaders are loaded."));
1581 return;
1582 }
1583 int foundIndex = -1;
1584
1585 for (int i = 0; i < items.size(); ++i) {
1586 if (items[i].compare(searchText, Qt::CaseInsensitive) == 0) {
1587 foundIndex = i;
1588 break;
1589 }
1590 }
1591
1592 if (foundIndex == -1) {
1593 for (int i = 0; i < items.size(); ++i) {
1594 if (items[i].contains(searchText, Qt::CaseInsensitive)) {
1595 foundIndex = i;
1596 break;
1597 }
1598 }
1599 }
1600
1601 if (foundIndex != -1) {
1602 lastFoundIndex = foundIndex;
1603 selectShaderRow(foundIndex);
1604 Log("Found shader: " + items[foundIndex] + " at index " + QString::number(foundIndex));
1605 } else {
1606 QMessageBox::information(this, tr("Not Found"), tr("Shader \"") + searchText + tr("\" not found in the list."));
1607 Log("Shader not found: " + searchText);
1608 }
1609}
1610
1612 if (lastSearchText.isEmpty()) {
1613 QMessageBox::information(this, tr("No Search"), tr("Please perform a search first (Ctrl+F)."));
1614 return;
1615 }
1616
1617 if (items.isEmpty()) {
1618 return;
1619 }
1620
1621 int foundIndex = -1;
1622 int startIndex = (lastFoundIndex + 1) % items.size();
1623
1624 for (int i = startIndex; i < items.size(); ++i) {
1625 if (items[i].contains(lastSearchText, Qt::CaseInsensitive)) {
1626 foundIndex = i;
1627 break;
1628 }
1629 }
1630
1631 if (foundIndex == -1 && startIndex > 0) {
1632 for (int i = 0; i < startIndex; ++i) {
1633 if (items[i].contains(lastSearchText, Qt::CaseInsensitive)) {
1634 foundIndex = i;
1635 break;
1636 }
1637 }
1638 }
1639
1640 if (foundIndex != -1) {
1641 lastFoundIndex = foundIndex;
1642 selectShaderRow(foundIndex);
1643 Log("Found next: " + items[foundIndex] + " at index " + QString::number(foundIndex));
1644 } else {
1645 QMessageBox::information(this, tr("No More Results"), tr("No more matches for \"") + lastSearchText + tr("\"."));
1646 Log("No more matches for: " + lastSearchText);
1647 }
1648}
1649
1652 QMessageBox::information(this, tr("New Shader File"), tr("Create or load a shader library first."));
1653 return;
1654 }
1656 QString typeError;
1657 const auto libraryType = acmx2::shader_manifest_library_type(shader_path, typeError);
1658 if (!typeError.isEmpty()) {
1659 QMessageBox::warning(this, tr("New Shader File"), typeError);
1660 return;
1661 }
1662 if (libraryType && *libraryType == acmx2::ShaderLibraryType::Runtime) {
1663 QMessageBox::information(this,
1664 tr("New Shader File"),
1665 tr("New ACMXVK shaders must be added to a source library, not "
1666 "a compiled SPIR-V runtime library."));
1667 return;
1668 }
1669 }
1670 ShaderDialog new_shader(active_backend, this);
1671 new_shader.setShaderPath(shader_path);
1672 if (new_shader.exec() == QDialog::Accepted) {
1673 QSettings appSettings("LostSideDead");
1674 appSettings.setValue(acmx2::backend_settings_key(active_backend, "library"), shader_path);
1676 appSettings.setValue("shaders", shader_path);
1677 appSettings.sync();
1678 loadShaders(shader_path, true);
1679 }
1680}
1681
1683 int row = currentShaderRow();
1684 if (row < 0 || row >= items.size())
1685 return;
1686 const QString shaderName = items.at(row);
1687 QString manifestError;
1688 if (!acmx2::remove_shader_manifest_entry(shader_path, shaderName, manifestError)) {
1689 QMessageBox::warning(this, tr("Could Not Remove Shader"), manifestError);
1690 Log(tr("Could not remove %1 from the library manifest: %2").arg(shaderName, manifestError));
1691 return;
1692 }
1693 items.removeAt(row);
1697 Log(tr("Removed shader from library manifest: %1").arg(shaderName));
1698 loadShaders(shader_path, true);
1699}
1700
1702 if (!process || process->state() != QProcess::Running)
1703 return;
1704 const int row = currentShaderRow();
1705 if (row < 0 || row >= items.size()) {
1706 Log("No shader selected.");
1707 return;
1708 }
1710}
1711
1713 QStringList writtenItems;
1714 const int rowCount = items.size();
1715
1716 for (int row = 0; row < rowCount; ++row) {
1717 const QString shaderName = items.at(row).trimmed();
1718 if (shaderName.isEmpty() || writtenItems.contains(shaderName, Qt::CaseInsensitive)) {
1719 continue;
1720 }
1721
1722 QString fullPath = shader_path + "/" + shaderName;
1723 QFileInfo fileInfo(fullPath);
1724 if (fileInfo.exists() && fileInfo.isFile()) {
1725 writtenItems.append(shaderName);
1726 } else {
1727 Log("Warning: File no longer exists, removing from list: " + shaderName);
1728 }
1729 }
1730 QString manifestError;
1731 QStringList existingItems;
1732 if (acmx2::load_shader_manifest(shader_path, existingItems, manifestError) && existingItems == writtenItems) {
1735 return;
1736 }
1737 manifestError.clear();
1738 if (!acmx2::write_shader_manifest(shader_path, writtenItems, manifestError)) {
1739 Log("Failed to update shader manifest: " + manifestError);
1740 return;
1741 }
1744
1745 if (writtenItems.size() != rowCount) {
1746 items = writtenItems;
1748 Log("Updated shader list, removed " + QString::number(rowCount - writtenItems.size()) + " non-existent files");
1749 }
1750}
1751
1753 const int row = currentShaderRow();
1754 if (row <= 0 || row >= items.size())
1755 return;
1756 items.swapItemsAt(row, row - 1);
1758 selectShaderRow(row - 1);
1759 updateIndex();
1760}
1761
1763 const int row = currentShaderRow();
1764 if (row < 0 || row >= items.size() - 1)
1765 return;
1766 items.swapItemsAt(row, row + 1);
1768 selectShaderRow(row + 1);
1769 updateIndex();
1770}
1771
1772QString MainWindow::readFileContents(const QString &filePath) {
1773 QFile file(filePath);
1774 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1775 Log("Failed to open file: " + filePath);
1776 return QString();
1777 }
1778
1779 QTextStream in(&file);
1780 QString contents = in.readAll();
1781 file.close();
1782 return contents;
1783}
1784
1785void MainWindow::listClicked(const QModelIndex &i) {
1786 if (!i.isValid())
1787 return;
1788 const int row = i.row();
1789 if (row < 0 || row >= items.size())
1790 return;
1791 QString itemText = sanitizeShaderName(items.at(row));
1792 if (itemText.isEmpty()) {
1793 Log("Invalid shader name");
1794 return;
1795 }
1796 QString filePath = shader_path + "/" + itemText;
1797 openShaderEditor(filePath);
1798}
1799
1800void MainWindow::openShaderEditor(const QString &filePath, int lineNumber, int columnNumber, int matchLength) {
1801 const QFileInfo requestedFile(filePath);
1802 if (!requestedFile.exists() || !requestedFile.isFile()) {
1803 QMessageBox::warning(this, tr("Open Shader"), tr("Shader file no longer exists:\n%1").arg(filePath));
1804 return;
1805 }
1806
1808 const QString canonicalPath = requestedFile.canonicalFilePath();
1809 for (const QPointer<TextEditor> &openEditor : open_files) {
1810 if (!openEditor)
1811 continue;
1812 const QString openPath = QFileInfo(openEditor->fileName()).canonicalFilePath();
1813 if (!canonicalPath.isEmpty() && openPath == canonicalPath) {
1815 shaderEditorTabs->setCurrentWidget(openEditor);
1816 shaderEditorWorkspace->show();
1817 shaderEditorWorkspace->raise();
1818 shaderEditorWorkspace->activateWindow();
1819 openEditor->revealLocation(lineNumber, columnNumber, matchLength);
1820 return;
1821 }
1822 }
1823
1825 TextEditor *editor = new TextEditor(shaderEditorTabs);
1826 editor->setWindowFlags(Qt::Widget);
1827 editor->setText(readFileContents(filePath));
1828 editor->setFileName(filePath);
1829 connect(editor, &TextEditor::fileSaved, this, [this](const QString &filePath) { handleSavedShader(filePath); });
1830 connect(editor, &TextEditor::openFileRequested, this, [this](const QString &includePath, int lineNumber) { openShaderEditor(includePath, lineNumber); });
1832 connect(editor, &TextEditor::uniformValueChanged, this, [this](const QString &name, double value) {
1834 return;
1835 }
1836 for (const QPointer<TextEditor> &openEditor : open_files) {
1837 if (openEditor)
1838 openEditor->setUniformValue(name, value);
1839 }
1840 });
1841 open_files.append(editor);
1842 const int tabIndex = shaderEditorTabs->addTab(editor, requestedFile.fileName());
1843 connect(editor, &QWidget::windowTitleChanged, this, [this, editor](const QString &title) {
1844 if (!shaderEditorTabs)
1845 return;
1846 const int index = shaderEditorTabs->indexOf(editor);
1847 if (index < 0)
1848 return;
1849 QString tabTitle = title;
1850 const int separator = tabTitle.indexOf(QStringLiteral(" - "));
1851 if (separator >= 0)
1852 tabTitle = tabTitle.mid(separator + 3);
1853 shaderEditorTabs->setTabText(index, tabTitle);
1854 });
1856 shaderEditorTabs->setCurrentIndex(tabIndex);
1857 shaderEditorWorkspace->show();
1858 shaderEditorWorkspace->raise();
1859 shaderEditorWorkspace->activateWindow();
1860 editor->show();
1861 editor->revealLocation(lineNumber, columnNumber, matchLength);
1862}
1863
1866 return;
1867 shaderEditorWorkspace = new QDialog(this);
1868 shaderEditorWorkspace->setWindowTitle(tr("ACMX Shader Editor"));
1869 shaderEditorWorkspace->setModal(false);
1870 auto *layout = new QVBoxLayout(shaderEditorWorkspace);
1871 layout->setContentsMargins(4, 4, 4, 4);
1872 shaderEditorTabs = new QTabWidget(shaderEditorWorkspace);
1873 shaderEditorTabs->setTabsClosable(true);
1874 shaderEditorTabs->setMovable(true);
1875 shaderEditorTabs->setDocumentMode(true);
1876 layout->addWidget(shaderEditorTabs);
1877 connect(shaderEditorTabs, &QTabWidget::tabCloseRequested, this, [this](int index) {
1878 auto *editor = qobject_cast<TextEditor *>(shaderEditorTabs->widget(index));
1879 if (editor && editor->close())
1880 shaderEditorTabs->removeTab(index);
1881 });
1882 QSettings settings("LostSideDead");
1883 if (!shaderEditorWorkspace->restoreGeometry(settings.value("editor/workspaceGeometry").toByteArray())) {
1884 shaderEditorWorkspace->resize(1180, 820);
1885 }
1886 connect(shaderEditorWorkspace, &QDialog::finished, this, [this](int) {
1888 QSettings("LostSideDead").setValue("editor/workspaceGeometry", shaderEditorWorkspace->saveGeometry());
1889 }
1890 });
1891}
1892
1893void MainWindow::updateOpenEditorCompileStatus(const QString &sourcePath, bool pending, bool success, const QString &diagnostics) {
1894 const QFileInfo sourceInfo(sourcePath);
1895 const QString sourceCanonical = sourceInfo.canonicalFilePath();
1896 for (const QPointer<TextEditor> &editor : open_files) {
1897 if (!editor)
1898 continue;
1899 const QFileInfo editorInfo(editor->fileName());
1900 const QString editorCanonical = editorInfo.canonicalFilePath();
1901 const bool sameFile = (!sourceCanonical.isEmpty() && !editorCanonical.isEmpty() && sourceCanonical == editorCanonical) || sourceInfo.absoluteFilePath() == editorInfo.absoluteFilePath();
1902 if (!sameFile)
1903 continue;
1904 if (pending)
1905 editor->setCompilePending();
1906 else
1907 editor->setCompileResult(success, diagnostics);
1908 }
1909}
1910
1913 QList<acmx2::CustomUniformDefinition> definitions;
1914 QString error;
1915 if (acmxvk && !shader_path.isEmpty() && !acmx2::load_custom_uniforms(shader_path, definitions, error)) {
1916 definitions.clear();
1917 }
1918
1919 QVector<ShaderEditorUniform> uniforms;
1920 uniforms.reserve(definitions.size());
1921 for (const acmx2::CustomUniformDefinition &definition : definitions)
1922 uniforms.append({definition.name, definition.slot, definition.minimum, definition.maximum, definition.step, definition.value});
1923
1924 const QString libraryRoot = QFileInfo(shader_path).canonicalFilePath();
1925 for (const QPointer<TextEditor> &editor : open_files) {
1926 if (!editor)
1927 continue;
1928 const QString editorPath = QFileInfo(editor->fileName()).canonicalFilePath();
1929 const QString relative = libraryRoot.isEmpty() || editorPath.isEmpty() ? QStringLiteral("..") : QDir(libraryRoot).relativeFilePath(editorPath);
1930 const bool inActiveLibrary = relative != QStringLiteral("..") && !relative.startsWith(QStringLiteral("../"));
1931 if (inActiveLibrary)
1932 editor->setShaderContext(acmxvk, uniforms, libraryRoot);
1933 }
1934}
1935
1937 QTreeWidgetItem *it = list_view ? list_view->currentItem() : nullptr;
1938 if (!it)
1939 return QString();
1940 const int row = list_view->indexOfTopLevelItem(it);
1941 if (row < 0 || row >= items.size())
1942 return it->text(1);
1943 return items.at(row);
1944}
1945
1947 if (!list_view)
1948 return -1;
1949 QTreeWidgetItem *it = list_view->currentItem();
1950 if (!it)
1951 return -1;
1952 return list_view->indexOfTopLevelItem(it);
1953}
1954
1956#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
1957#if defined(__linux__) || defined(__APPLE__)
1958 // A named semaphore can be unlinked while an existing process still owns
1959 // a usable handle. Verify that new child processes can still discover the
1960 // name before every launch and recreate it when necessary.
1961 if (shaderSelectionSemaphore != SEM_FAILED) {
1962 sem_t *publishedSemaphore = ::sem_open(acmx2::ipc::kShaderSelectionSemaphoreName, 0);
1963 if (publishedSemaphore != SEM_FAILED) {
1964 ::sem_close(publishedSemaphore);
1965 } else {
1966 ::sem_close(shaderSelectionSemaphore);
1967 shaderSelectionSemaphore = SEM_FAILED;
1968 }
1969 }
1970 if (shaderSelectionSemaphore == SEM_FAILED) {
1971 shaderSelectionSemaphore = ::sem_open(acmx2::ipc::kShaderSelectionSemaphoreName, O_CREAT, 0666, 1);
1972 }
1973 if (shaderSelectionSemaphore == SEM_FAILED) {
1974 Log(tr("Shared interface control unavailable: sem_open(%1) failed: %2").arg(acmx2::ipc::kShaderSelectionSemaphoreName, QString::fromLocal8Bit(std::strerror(errno))));
1975 return;
1976 }
1977
1978 if (shaderSelectionShm)
1979 return;
1980
1981 shaderSelectionShmFd = ::shm_open(acmx2::ipc::kShaderSelectionShmName, O_CREAT | O_RDWR, 0666);
1982 if (shaderSelectionShmFd < 0) {
1983 const int openError = errno;
1984 Log(tr("Shared interface control unavailable: shm_open(%1) failed: "
1985 "%2")
1986 .arg(acmx2::ipc::kShaderSelectionShmName, QString::fromLocal8Bit(std::strerror(openError))));
1988 return;
1989 }
1990
1991 constexpr std::size_t SHARED_MEMORY_SIZE = sizeof(acmx2::ipc::ShaderSelectionShmData);
1992 struct stat shmStat{};
1993 if (::fstat(shaderSelectionShmFd, &shmStat) != 0) {
1994 const int statError = errno;
1995 Log(tr("Shared interface control unavailable: fstat(%1) failed: %2").arg(acmx2::ipc::kShaderSelectionShmName, QString::fromLocal8Bit(std::strerror(statError))));
1997 return;
1998 }
1999
2000 if (shmStat.st_size == 0) {
2001 if (::ftruncate(shaderSelectionShmFd, static_cast<off_t>(SHARED_MEMORY_SIZE)) != 0) {
2002 const int truncateError = errno;
2003 Log(tr("Shared interface control unavailable: ftruncate(%1, %2) "
2004 "failed: %3")
2006 .arg(static_cast<qulonglong>(SHARED_MEMORY_SIZE))
2007 .arg(QString::fromLocal8Bit(std::strerror(truncateError))));
2009 return;
2010 }
2011 } else if (shmStat.st_size < static_cast<off_t>(SHARED_MEMORY_SIZE)) {
2012 Log(tr("Shared interface control unavailable: %1 has size %2 bytes; "
2013 "expected %3. Refusing to resize an active or stale shared "
2014 "memory object.")
2016 .arg(static_cast<qlonglong>(shmStat.st_size))
2017 .arg(static_cast<qulonglong>(SHARED_MEMORY_SIZE)));
2019 return;
2020 }
2021
2022 void *mapped = ::mmap(nullptr, SHARED_MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shaderSelectionShmFd, 0);
2023 if (mapped == MAP_FAILED) {
2024 const int mapError = errno;
2025 Log(tr("Shared interface control unavailable: mmap(%1, %2) failed: "
2026 "%3")
2028 .arg(static_cast<qulonglong>(SHARED_MEMORY_SIZE))
2029 .arg(QString::fromLocal8Bit(std::strerror(mapError))));
2031 return;
2032 }
2033
2034 shaderSelectionShm = static_cast<acmx2::ipc::ShaderSelectionShmData *>(mapped);
2035#else
2036 if (shaderSelectionSemaphore == nullptr) {
2037 shaderSelectionSemaphore = ::CreateMutexW(nullptr, FALSE, acmx2::ipc::kShaderSelectionMutexNameWindows);
2038 }
2039 if (shaderSelectionSemaphore == nullptr) {
2040 Log(tr("Shared interface control unavailable: CreateMutexW failed "
2041 "with Windows error %1")
2042 .arg(static_cast<qulonglong>(::GetLastError())));
2043 return;
2044 }
2045
2046 if (shaderSelectionShm)
2047 return;
2048
2049 constexpr std::size_t SHARED_MEMORY_SIZE = sizeof(acmx2::ipc::ShaderSelectionShmData);
2050 shaderSelectionMapping = ::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, static_cast<DWORD>(SHARED_MEMORY_SIZE), acmx2::ipc::kShaderSelectionMappingNameWindows);
2051 if (shaderSelectionMapping == nullptr) {
2052 Log(tr("Shared interface control unavailable: CreateFileMappingW "
2053 "failed with Windows error %1")
2054 .arg(static_cast<qulonglong>(::GetLastError())));
2056 return;
2057 }
2058
2059 void *mapped = ::MapViewOfFile(shaderSelectionMapping, FILE_MAP_ALL_ACCESS, 0, 0, SHARED_MEMORY_SIZE);
2060 if (mapped == nullptr) {
2061 Log(tr("Shared interface control unavailable: MapViewOfFile failed "
2062 "with Windows error %1")
2063 .arg(static_cast<qulonglong>(::GetLastError())));
2065 return;
2066 }
2067 shaderSelectionShm = static_cast<acmx2::ipc::ShaderSelectionShmData *>(mapped);
2068#endif
2069
2070 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
2071 if (!lock) {
2072#if defined(__linux__) || defined(__APPLE__)
2073 Log(tr("Shared interface control unavailable: could not lock %1: %2").arg(acmx2::ipc::kShaderSelectionSemaphoreName, QString::fromLocal8Bit(std::strerror(errno))));
2074#else
2075 Log(tr("Shared interface control unavailable: could not lock the "
2076 "Windows control mutex (error %1)")
2077 .arg(static_cast<qulonglong>(::GetLastError())));
2078#endif
2080 return;
2081 }
2082
2083 if (shaderSelectionShm->magic != acmx2::ipc::kShaderSelectionMagic || shaderSelectionShm->version != acmx2::ipc::kShaderSelectionVersion) {
2084 shaderSelectionShm->magic = acmx2::ipc::kShaderSelectionMagic;
2085 shaderSelectionShm->version = acmx2::ipc::kShaderSelectionVersion;
2086 shaderSelectionShm->selected_index = -1;
2087 shaderSelectionShm->shader_pass_count = 0;
2088 shaderSelectionShm->shader_pass_enabled = 0;
2089 shaderSelectionShm->repeat_enabled = 0;
2090 shaderSelectionShm->display_filter_enabled = 0;
2091 shaderSelectionShm->watermark_enabled = 0;
2092 shaderSelectionShm->normalized_time_enabled = 0;
2093 std::fill(std::begin(shaderSelectionShm->reserved_flags), std::end(shaderSelectionShm->reserved_flags), 0);
2094 std::fill(std::begin(shaderSelectionShm->shader_pass_indices), std::end(shaderSelectionShm->shader_pass_indices), -1);
2095 std::fill(&shaderSelectionShm->shader_pass_names[0][0], &shaderSelectionShm->shader_pass_names[0][0] + acmx2::ipc::kShaderSelectionMaxPassCount * acmx2::ipc::kShaderSelectionMaxShaderName, '\0');
2096 shaderSelectionShm->gpu_filter_count = 0;
2097 shaderSelectionShm->gpu_filter_enabled = 0;
2098 shaderSelectionShm->gpu_buffer_size = 8;
2099 shaderSelectionShm->watermark_r = 255;
2100 shaderSelectionShm->watermark_g = 0;
2101 shaderSelectionShm->watermark_b = 150;
2102 std::fill(std::begin(shaderSelectionShm->reserved), std::end(shaderSelectionShm->reserved), 0);
2103 std::fill(std::begin(shaderSelectionShm->gpu_filter_indices), std::end(shaderSelectionShm->gpu_filter_indices), -1);
2104 std::fill(std::begin(shaderSelectionShm->watermark_text), std::end(shaderSelectionShm->watermark_text), '\0');
2105 shaderSelectionShm->reload_shader_index = -1;
2106 std::fill(std::begin(shaderSelectionShm->reload_shader_path), std::end(shaderSelectionShm->reload_shader_path), '\0');
2107 shaderSelectionShm->reload_sequence = 0;
2108 shaderSelectionShm->custom_uniform_count = 0;
2109 std::fill(&shaderSelectionShm->custom_uniform_names[0][0], &shaderSelectionShm->custom_uniform_names[0][0] + acmx2::ipc::kShaderSelectionMaxCustomUniforms * acmx2::ipc::kShaderSelectionMaxUniformName, '\0');
2110 std::fill(std::begin(shaderSelectionShm->custom_uniform_values), std::end(shaderSelectionShm->custom_uniform_values), 0.0f);
2111 std::fill(std::begin(shaderSelectionShm->audio_file_path), std::end(shaderSelectionShm->audio_file_path), '\0');
2112 shaderSelectionShm->audio_output_device = -1;
2113 shaderSelectionShm->audio_pass_through = 0;
2114 shaderSelectionShm->audio_trunc = 0;
2115 shaderSelectionShm->audio_repeat = 0;
2116 shaderSelectionShm->audio_reserved = 0;
2117 shaderSelectionShm->audio_file_sequence = 0;
2118 shaderSelectionShm->dream_enabled = 0;
2119 shaderSelectionShm->dream_fp16 = 0;
2120 shaderSelectionShm->dream_gpu_filter_first = 0;
2121 shaderSelectionShm->dream_reserved = 0;
2122 shaderSelectionShm->dream_iterations = 1;
2123 shaderSelectionShm->dream_maximum_dimension = 512;
2124 shaderSelectionShm->dream_channel = -1;
2125 shaderSelectionShm->dream_octaves = 1;
2126 shaderSelectionShm->dream_jitter = 0;
2127 shaderSelectionShm->dream_smoothing = 0;
2128 shaderSelectionShm->dream_strength = 0.05F;
2129 shaderSelectionShm->dream_feedback = 0.9F;
2130 shaderSelectionShm->dream_zoom = 1.01F;
2131 shaderSelectionShm->dream_rotation = 0.1F;
2132 shaderSelectionShm->dream_octave_scale = 1.4F;
2133 std::fill(std::begin(shaderSelectionShm->dream_model_path), std::end(shaderSelectionShm->dream_model_path), '\0');
2134 std::fill(std::begin(shaderSelectionShm->dream_layer), std::end(shaderSelectionShm->dream_layer), '\0');
2135 std::fill(std::begin(shaderSelectionShm->selected_shader_name), std::end(shaderSelectionShm->selected_shader_name), '\0');
2136 shaderSelectionShm->sequence = 0;
2137 }
2138#endif
2139}
2140
2142#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2143 if (!shaderSelectionShm)
2144 return;
2145 const int row = currentShaderRow();
2146 if (row < 0 || row >= items.size())
2147 return;
2148 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
2149 if (!lock) {
2150 Log("<br><style color=\"red\">Error lock failed</style><br>");
2151 return;
2152 }
2153 shaderSelectionShm->selected_index = row;
2154 const QByteArray shaderName = items.at(row).toUtf8();
2155 const qsizetype copyLength = std::min<qsizetype>(shaderName.size(), static_cast<qsizetype>(acmx2::ipc::kShaderSelectionMaxShaderName - 1));
2156 std::fill(std::begin(shaderSelectionShm->selected_shader_name), std::end(shaderSelectionShm->selected_shader_name), '\0');
2157 std::copy_n(shaderName.constData(), copyLength, shaderSelectionShm->selected_shader_name);
2158 ++shaderSelectionShm->sequence;
2159#endif
2160}
2161
2163#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2164 if (active_backend != acmx2::Backend::Acmx2 || !shaderSelectionShm || !process || process->state() != QProcess::Running || cacheBuildInProgress) {
2165 return;
2166 }
2167
2168 const QFileInfo savedFile(filePath);
2169 const QString shaderName = QDir(shader_path).relativeFilePath(savedFile.absoluteFilePath());
2170 const int shaderIndex = items.indexOf(shaderName, 0, Qt::CaseInsensitive);
2171 if (shaderIndex < 0) {
2172 Log("Saved shader is not in the active library; live reload was skipped: " + filePath);
2173 return;
2174 }
2175
2176 const QByteArray reloadPath = savedFile.canonicalFilePath().toUtf8();
2177 if (reloadPath.isEmpty() || reloadPath.size() >= static_cast<int>(acmx2::ipc::kShaderSelectionMaxReloadPath)) {
2178 Log("Shader path is too long for live reload: " + filePath);
2179 return;
2180 }
2181
2182 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
2183 if (!lock) {
2184 Log("<br><style color=\"red\">Error lock failed</style><br>");
2185 return;
2186 }
2187 shaderSelectionShm->reload_shader_index = shaderIndex;
2188 std::fill(std::begin(shaderSelectionShm->reload_shader_path), std::end(shaderSelectionShm->reload_shader_path), '\0');
2189 std::copy(reloadPath.cbegin(), reloadPath.cend(), shaderSelectionShm->reload_shader_path);
2190 ++shaderSelectionShm->reload_sequence;
2191 ++shaderSelectionShm->sequence;
2192 Log("Requested live shader reload: " + shaderName + "<br>");
2193#else
2194 Q_UNUSED(filePath);
2195#endif
2196}
2197
2198void MainWindow::handleSavedShader(const QString &filePath) {
2201 queueAcmxvkLiveCompile(filePath);
2202 return;
2203 }
2205}
2206
2207void MainWindow::queueAcmxvkLiveCompile(const QString &filePath) {
2208#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2209 QString typeError;
2210 if (!is_acmxvk_source_library(shader_path, typeError)) {
2211 const QString diagnostic = typeError.isEmpty() ? tr("ACMXVK live compile requires a source library.") : typeError;
2212 Log(diagnostic);
2213 updateOpenEditorCompileStatus(filePath, false, false, diagnostic);
2214 return;
2215 }
2216
2217 const QFileInfo sourceInfo(filePath);
2218 const QString sourcePath = sourceInfo.canonicalFilePath();
2219 const QString sourceRoot = QFileInfo(shader_path).canonicalFilePath();
2220 if (sourcePath.isEmpty() || sourceRoot.isEmpty()) {
2221 const QString diagnostic = tr("Could not resolve saved ACMXVK shader: %1").arg(filePath);
2222 Log(diagnostic);
2223 updateOpenEditorCompileStatus(filePath, false, false, diagnostic);
2224 return;
2225 }
2226 const QString sourceName = sanitizeShaderName(QDir(sourceRoot).relativeFilePath(sourcePath));
2227 if (sourceName.isEmpty() || (!sourceName.endsWith(QStringLiteral(".frag"), Qt::CaseInsensitive) && !sourceName.endsWith(QStringLiteral(".comp"), Qt::CaseInsensitive)) || !items.contains(sourceName, Qt::CaseInsensitive)) {
2228 const QString diagnostic = tr("Saved file is not a fragment or compute source in the active "
2229 "ACMXVK library: %1")
2230 .arg(filePath);
2231 Log(diagnostic);
2232 updateOpenEditorCompileStatus(filePath, false, false, diagnostic);
2233 return;
2234 }
2235
2236 liveShaderCompileQueue.removeAll(sourcePath);
2237 liveShaderCompileQueue.append(sourcePath);
2238 updateOpenEditorCompileStatus(sourcePath, true);
2240#else
2241 Q_UNUSED(filePath);
2242#endif
2243}
2244
2246#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2247 if (liveShaderCompileProcess && liveShaderCompileProcess->state() != QProcess::NotRunning) {
2248 return;
2249 }
2250 if (liveShaderCompileQueue.isEmpty()) {
2251 return;
2252 }
2253
2255 liveShaderCompileProcess = new QProcess(this);
2256 liveShaderCompileProcess->setProcessChannelMode(QProcess::SeparateChannels);
2257 connect(liveShaderCompileProcess, &QProcess::readyReadStandardOutput, this, [this]() {
2258 QString output = QString::fromUtf8(liveShaderCompileProcess->readAllStandardOutput());
2259 liveShaderCompileStdout += output;
2260 if (liveShaderCompileStdout.size() > 262144)
2262 Write(output.toHtmlEscaped().replace('\n', QStringLiteral("<br>")));
2263 });
2264 connect(liveShaderCompileProcess, &QProcess::readyReadStandardError, this, [this]() {
2265 QString output = QString::fromUtf8(liveShaderCompileProcess->readAllStandardError());
2266 liveShaderCompileStderr += output;
2267 if (liveShaderCompileStderr.size() > 262144)
2269 Write(QStringLiteral("<b style='color:red;'>") + output.toHtmlEscaped().replace('\n', QStringLiteral("<br>")) + QStringLiteral("</b>"));
2270 });
2271 connect(liveShaderCompileProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
2272 liveShaderCompileStdout += QString::fromUtf8(liveShaderCompileProcess->readAllStandardOutput());
2273 liveShaderCompileStderr += QString::fromUtf8(liveShaderCompileProcess->readAllStandardError());
2274 bool installed = false;
2275 QString editorDiagnostics;
2276 QString compilerOutput = liveShaderCompileStderr.trimmed();
2277 if (!liveShaderCompileStdout.trimmed().isEmpty()) {
2278 if (!compilerOutput.isEmpty())
2279 compilerOutput += QLatin1Char('\n');
2280 compilerOutput += liveShaderCompileStdout.trimmed();
2281 }
2282 if (exitStatus == QProcess::NormalExit && exitCode == 0) {
2283 QFile compiled(liveShaderCompileTemporary);
2284 quint32 magic = 0;
2285 if (compiled.open(QIODevice::ReadOnly)) {
2286 QDataStream stream(&compiled);
2287 stream.setByteOrder(QDataStream::LittleEndian);
2288 stream >> magic;
2289 }
2290 compiled.close();
2291 constexpr quint32 SPIRV_MAGIC = 0x07230203U;
2292 if (magic != SPIRV_MAGIC) {
2293 editorDiagnostics = tr("Compiler did not produce valid SPIR-V.");
2294 Log(tr("<b style='color:red;'>Live ACMXVK compile did "
2295 "not produce valid SPIR-V for %1.</b>")
2297 } else if (QFileInfo(liveShaderCompileOutput).isSymLink()) {
2298 editorDiagnostics = tr("The compiled output is a symbolic link and cannot "
2299 "be replaced safely.");
2300 Log(tr("<b style='color:red;'>Refusing to replace "
2301 "symbolic-link shader output: %1</b>")
2303 } else {
2304 std::error_code error;
2305 replace_file(std::filesystem::u8path(liveShaderCompileTemporary.toUtf8().constData()), std::filesystem::u8path(liveShaderCompileOutput.toUtf8().constData()), error);
2306 if (error) {
2307 editorDiagnostics = tr("Could not install compiled shader: %1").arg(QString::fromStdString(error.message()));
2308 Log(tr("<b style='color:red;'>Could not install "
2309 "live ACMXVK shader: %1</b>")
2310 .arg(QString::fromStdString(error.message())));
2311 } else {
2312 installed = true;
2313 }
2314 }
2315 } else {
2316 Log(tr("<b style='color:red;'>Live ACMXVK compile failed "
2317 "for %1 (%2, exit code %3).</b>")
2319 .arg(exitStatus == QProcess::CrashExit ? tr("compiler crashed") : tr("compiler error"))
2320 .arg(exitCode));
2321 if (compilerOutput.isEmpty())
2322 compilerOutput = liveShaderCompileProcess->errorString();
2323 editorDiagnostics = compilerOutput;
2324 Log(tr("<b style='color:red;'>Compiler message:</b>"
2325 "<pre style='white-space:pre-wrap;'>%1</pre>")
2326 .arg(compilerOutput.toHtmlEscaped()));
2327 }
2328
2329 updateOpenEditorCompileStatus(liveShaderCompileSource, false, installed, installed ? compilerOutput : editorDiagnostics);
2330
2331 QFile::remove(liveShaderCompileInput);
2332 if (!installed) {
2333 QFile::remove(liveShaderCompileTemporary);
2334 } else {
2335 Log(tr("Compiled and installed ACMXVK shader: %1").arg(liveShaderCompileOutput));
2338 }
2339
2341 liveShaderCompileInput.clear();
2346 QTimer::singleShot(0, this, &MainWindow::startNextAcmxvkLiveCompile);
2347 });
2348 }
2349
2350 QString compilerError;
2351 const QString glslc = resolve_acmxvk_shader_compiler(compilerError);
2352 if (glslc.isEmpty()) {
2353 Log(tr("<b style='color:red;'>Cannot live compile ACMXVK shader: "
2354 "%1</b>")
2355 .arg(compilerError.toHtmlEscaped()));
2356 for (const QString &sourcePath : liveShaderCompileQueue) {
2357 updateOpenEditorCompileStatus(sourcePath, false, false, compilerError);
2358 }
2359 liveShaderCompileQueue.clear();
2360 return;
2361 }
2362
2365 const QString sourceRoot = QFileInfo(shader_path).canonicalFilePath();
2366 const QString sourceName = QDir(sourceRoot).relativeFilePath(liveShaderCompileSource);
2367 liveShaderCompileOutput = QDir(acmxvk_build_directory(sourceRoot)).filePath(acmxvk_runtime_shader_name(sourceName));
2368 if (!QDir().mkpath(QFileInfo(liveShaderCompileOutput).absolutePath())) {
2369 const QString diagnostic = tr("Could not create live shader output directory for %1.").arg(liveShaderCompileOutput);
2370 Log(QStringLiteral("<b style='color:red;'>%1</b>").arg(diagnostic.toHtmlEscaped()));
2371 updateOpenEditorCompileStatus(liveShaderCompileSource, false, false, diagnostic);
2374 QTimer::singleShot(0, this, &MainWindow::startNextAcmxvkLiveCompile);
2375 return;
2376 }
2377
2378 liveShaderCompileTemporary = liveShaderCompileOutput + QStringLiteral(".live-tmp-%1-%2").arg(QCoreApplication::applicationPid()).arg(++liveShaderCompileSequence);
2379 const QFileInfo sourceInfo(liveShaderCompileSource);
2380 liveShaderCompileInput = sourceInfo.absolutePath() + QLatin1Char('/') + QStringLiteral(".acmxvk-live-%1-%2.%3").arg(QCoreApplication::applicationPid()).arg(liveShaderCompileSequence).arg(sourceInfo.suffix());
2381 QFile sourceFile(liveShaderCompileSource);
2382 if (!sourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
2383 const QString diagnostic = tr("Could not read shader source for live safety processing: %1").arg(sourceFile.errorString());
2384 Log(QStringLiteral("<b style='color:red;'>%1</b>").arg(diagnostic.toHtmlEscaped()));
2385 updateOpenEditorCompileStatus(liveShaderCompileSource, false, false, diagnostic);
2387 liveShaderCompileInput.clear();
2389 QTimer::singleShot(0, this, &MainWindow::startNextAcmxvkLiveCompile);
2390 return;
2391 }
2392 const QString source = QString::fromUtf8(sourceFile.readAll());
2393 const QSettings editorSettings("LostSideDead");
2394 const QString guardedSource = editorSettings.value("editor/guardLoops", true).toBool() ? inject_safety_counters(source) : source;
2395 sourceFile.close();
2396 QSaveFile guardedFile(liveShaderCompileInput);
2397 const QByteArray guardedBytes = guardedSource.toUtf8();
2398 if (!guardedFile.open(QIODevice::WriteOnly | QIODevice::Text) || guardedFile.write(guardedBytes) != guardedBytes.size() || !guardedFile.commit()) {
2399 const QString diagnostic = tr("Could not create temporary shader source for live safety processing.");
2400 Log(QStringLiteral("<b style='color:red;'>%1</b>").arg(diagnostic.toHtmlEscaped()));
2401 updateOpenEditorCompileStatus(liveShaderCompileSource, false, false, diagnostic);
2402 QFile::remove(liveShaderCompileInput);
2404 liveShaderCompileInput.clear();
2406 QTimer::singleShot(0, this, &MainWindow::startNextAcmxvkLiveCompile);
2407 return;
2408 }
2409 const QStringList arguments{QStringLiteral("-I"), sourceRoot, liveShaderCompileInput, QStringLiteral("-o"), liveShaderCompileTemporary};
2410 Log(tr("Live compiling ACMXVK shader: %1").arg(sourceName));
2411 Log(tr("Command: %1 %2<br>").arg(glslc, concatList(arguments)));
2414 liveShaderCompileProcess->start(glslc, arguments);
2415 if (!liveShaderCompileProcess->waitForStarted()) {
2416 const QString diagnostic = tr("Failed to start the ACMXVK shader compiler: %1").arg(liveShaderCompileProcess->errorString());
2417 Log(QStringLiteral("<b style='color:red;'>%1</b>").arg(diagnostic.toHtmlEscaped()));
2418 updateOpenEditorCompileStatus(liveShaderCompileSource, false, false, diagnostic);
2419 QFile::remove(liveShaderCompileTemporary);
2420 QFile::remove(liveShaderCompileInput);
2422 liveShaderCompileInput.clear();
2427 QTimer::singleShot(0, this, &MainWindow::startNextAcmxvkLiveCompile);
2428 }
2429#endif
2430}
2431
2432void MainWindow::queueAcmxvkEditorPreview(const QString &filePath, const QString &source) {
2434 publishAcmx2EditorPreview(filePath, source);
2435 return;
2436 }
2438 return;
2439 pendingEditorPreviewPath = QFileInfo(filePath).absoluteFilePath();
2442}
2443
2444bool MainWindow::publishAcmx2EditorPreview(const QString &filePath, const QString &source) {
2445#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2446 if (!shaderSelectionShm || !process || process->state() != QProcess::Running) {
2447 const QString error = tr("Start ACMX2 before using shader live preview.");
2448 updateOpenEditorCompileStatus(filePath, false, false, error);
2449 Log(error);
2450 return false;
2451 }
2452
2453 const QFileInfo sourceInfo(filePath);
2454 const QString sourcePath = sourceInfo.canonicalFilePath();
2455 const QString sourceRoot = QFileInfo(shader_path).canonicalFilePath();
2456 if (sourcePath.isEmpty() || sourceRoot.isEmpty()) {
2457 const QString error = tr("Could not resolve the ACMX2 shader preview path: %1").arg(filePath);
2458 updateOpenEditorCompileStatus(filePath, false, false, error);
2459 Log(error);
2460 return false;
2461 }
2462
2463 const QString shaderName = sanitizeShaderName(QDir(sourceRoot).relativeFilePath(sourcePath));
2464 const int shaderIndex = items.indexOf(shaderName, 0, Qt::CaseInsensitive);
2465 const QString suffix = sourceInfo.suffix().toLower();
2466 if (shaderIndex < 0 || (suffix != QStringLiteral("glsl") && suffix != QStringLiteral("frag") && suffix != QStringLiteral("comp"))) {
2467 const QString error = tr("The editor file is not an active ACMX2 fragment or compute "
2468 "shader: %1")
2469 .arg(filePath);
2470 updateOpenEditorCompileStatus(filePath, false, false, error);
2471 Log(error);
2472 return false;
2473 }
2474
2475 const QString previewDirectory = QDir(sourceRoot).filePath(QStringLiteral(".acmx2-editor-preview"));
2476 if (!QDir().mkpath(previewDirectory)) {
2477 const QString error = tr("Could not create the ACMX2 editor preview directory.");
2478 updateOpenEditorCompileStatus(filePath, false, false, error);
2479 Log(error);
2480 return false;
2481 }
2482
2483 const QString previewPath = QDir(previewDirectory).filePath(QStringLiteral("preview-%1-%2.%3").arg(QCoreApplication::applicationPid()).arg(++editorPreviewSequence).arg(suffix));
2484 QSaveFile previewFile(previewPath);
2485 const QByteArray sourceBytes = source.toUtf8();
2486 if (!previewFile.open(QIODevice::WriteOnly | QIODevice::Text) || previewFile.write(sourceBytes) != sourceBytes.size() || !previewFile.commit()) {
2487 const QString error = tr("Could not write the temporary ACMX2 shader preview.");
2488 updateOpenEditorCompileStatus(filePath, false, false, error);
2489 Log(error);
2490 return false;
2491 }
2492
2493 const QByteArray reloadPath = QFileInfo(previewPath).canonicalFilePath().toUtf8();
2494 if (reloadPath.isEmpty() || reloadPath.size() >= static_cast<int>(acmx2::ipc::kShaderSelectionMaxReloadPath)) {
2495 QFile::remove(previewPath);
2496 const QString error = tr("The ACMX2 shader preview path is too long.");
2497 updateOpenEditorCompileStatus(filePath, false, false, error);
2498 Log(error);
2499 return false;
2500 }
2501
2502 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
2503 if (!lock) {
2504 QFile::remove(previewPath);
2505 const QString error = tr("Could not lock ACMX2 interface control.");
2506 updateOpenEditorCompileStatus(filePath, false, false, error);
2507 Log(error);
2508 return false;
2509 }
2510 shaderSelectionShm->reload_shader_index = shaderIndex;
2511 std::fill(std::begin(shaderSelectionShm->reload_shader_path), std::end(shaderSelectionShm->reload_shader_path), '\0');
2512 std::copy(reloadPath.cbegin(), reloadPath.cend(), shaderSelectionShm->reload_shader_path);
2513 ++shaderSelectionShm->reload_sequence;
2514 ++shaderSelectionShm->sequence;
2515
2516 editorPreviewTemporaryFiles.append(previewPath);
2517 while (editorPreviewTemporaryFiles.size() > 16)
2518 QFile::remove(editorPreviewTemporaryFiles.takeFirst());
2519 updateOpenEditorCompileStatus(filePath, false, true, tr("Preview source sent to the running ACMX2 backend."));
2520 Log(tr("Requested ACMX2 editor preview: %1").arg(shaderName));
2521 return true;
2522#else
2523 Q_UNUSED(filePath);
2524 Q_UNUSED(source);
2525 return false;
2526#endif
2527}
2528
2530 if (editorPreviewProcess && editorPreviewProcess->state() != QProcess::NotRunning) {
2531 return;
2532 }
2533 if (pendingEditorPreviewPath.isEmpty())
2534 return;
2535
2536 if (!editorPreviewProcess) {
2537 editorPreviewProcess = new QProcess(this);
2538 editorPreviewProcess->setProcessChannelMode(QProcess::SeparateChannels);
2539 connect(editorPreviewProcess, &QProcess::readyReadStandardOutput, this, [this]() { editorPreviewStdout += QString::fromUtf8(editorPreviewProcess->readAllStandardOutput()); });
2540 connect(editorPreviewProcess, &QProcess::readyReadStandardError, this, [this]() { editorPreviewStderr += QString::fromUtf8(editorPreviewProcess->readAllStandardError()); });
2541 connect(editorPreviewProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
2542 editorPreviewStdout += QString::fromUtf8(editorPreviewProcess->readAllStandardOutput());
2543 editorPreviewStderr += QString::fromUtf8(editorPreviewProcess->readAllStandardError());
2544 QString diagnostics = editorPreviewStderr.trimmed();
2545 if (!editorPreviewStdout.trimmed().isEmpty()) {
2546 if (!diagnostics.isEmpty())
2547 diagnostics += QLatin1Char('\n');
2548 diagnostics += editorPreviewStdout.trimmed();
2549 }
2550 diagnostics.replace(editorPreviewInput, editorPreviewPath);
2551 const bool success = exitStatus == QProcess::NormalExit && exitCode == 0 && QFileInfo(editorPreviewOutput).isFile() && QFileInfo(editorPreviewOutput).size() >= 20;
2552 updateOpenEditorCompileStatus(editorPreviewPath, false, success, diagnostics);
2553 if (success) {
2555 while (editorPreviewTemporaryFiles.size() > 16)
2556 QFile::remove(editorPreviewTemporaryFiles.takeFirst());
2558 Log(tr("Compiled ACMXVK editor preview: %1").arg(QFileInfo(editorPreviewPath).fileName()));
2559 } else {
2560 QFile::remove(editorPreviewOutput);
2561 Log(tr("<b style='color:red;'>ACMXVK editor preview failed: "
2562 "%1</b><pre style='white-space:pre-wrap;'>%2</pre>")
2563 .arg(QFileInfo(editorPreviewPath).fileName(), diagnostics.toHtmlEscaped()));
2564 }
2565 QFile::remove(editorPreviewInput);
2566 editorPreviewPath.clear();
2567 editorPreviewInput.clear();
2568 editorPreviewOutput.clear();
2569 editorPreviewStdout.clear();
2570 editorPreviewStderr.clear();
2571 QTimer::singleShot(0, this, &MainWindow::startNextAcmxvkEditorPreview);
2572 });
2573 }
2574
2575 QString compilerError;
2576 const QString glslc = resolve_acmxvk_shader_compiler(compilerError);
2578 const QString source = pendingEditorPreviewSource;
2581 if (glslc.isEmpty()) {
2582 updateOpenEditorCompileStatus(editorPreviewPath, false, false, compilerError);
2583 editorPreviewPath.clear();
2584 return;
2585 }
2586
2587 const QFileInfo sourceInfo(editorPreviewPath);
2588 const QString sourceRoot = QFileInfo(shader_path).canonicalFilePath();
2589 const QString previewDirectory = QDir(acmxvk_build_directory(sourceRoot)).filePath(QStringLiteral(".editor-preview"));
2590 if (!QDir().mkpath(previewDirectory)) {
2591 const QString error = tr("Could not create the editor preview directory.");
2593 editorPreviewPath.clear();
2594 return;
2595 }
2596 const QString suffix = sourceInfo.suffix().toLower();
2597 const QString baseName = QStringLiteral("preview-%1-%2.%3").arg(QCoreApplication::applicationPid()).arg(++editorPreviewSequence).arg(suffix == QStringLiteral("comp") ? QStringLiteral("comp") : QStringLiteral("frag"));
2598 editorPreviewInput = QDir(previewDirectory).filePath(baseName);
2599 editorPreviewOutput = editorPreviewInput + QStringLiteral(".spv");
2600 QSaveFile inputFile(editorPreviewInput);
2601 const QByteArray sourceBytes = source.toUtf8();
2602 if (!inputFile.open(QIODevice::WriteOnly | QIODevice::Text) || inputFile.write(sourceBytes) != sourceBytes.size() || !inputFile.commit()) {
2603 const QString error = tr("Could not write the temporary preview source.");
2605 editorPreviewPath.clear();
2606 editorPreviewInput.clear();
2607 editorPreviewOutput.clear();
2608 return;
2609 }
2610
2612 QStringList arguments{QStringLiteral("-I"), sourceInfo.absolutePath()};
2613 if (!sourceRoot.isEmpty() && sourceRoot != sourceInfo.absolutePath())
2614 arguments << QStringLiteral("-I") << sourceRoot;
2615 arguments << editorPreviewInput << QStringLiteral("-o") << editorPreviewOutput;
2616 editorPreviewStdout.clear();
2617 editorPreviewStderr.clear();
2618 editorPreviewProcess->start(glslc, arguments);
2619 if (!editorPreviewProcess->waitForStarted()) {
2620 const QString error = tr("Failed to start the shader compiler: %1").arg(editorPreviewProcess->errorString());
2622 QFile::remove(editorPreviewInput);
2623 editorPreviewPath.clear();
2624 editorPreviewInput.clear();
2625 editorPreviewOutput.clear();
2626 }
2627}
2628
2629void MainWindow::publishAcmxvkCompiledShaderReload(const QString &sourcePath, const QString &runtimePath) {
2630#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2631 if (active_backend != acmx2::Backend::Acmxvk || !shaderSelectionShm || !process || process->state() != QProcess::Running) {
2632 return;
2633 }
2634
2635 const QString sourceRoot = QFileInfo(shader_path).canonicalFilePath();
2636 const QString resolvedSourcePath = QFileInfo(sourcePath).canonicalFilePath();
2637 const QString sourceName = sourceRoot.isEmpty() || resolvedSourcePath.isEmpty() ? QString() : sanitizeShaderName(QDir(sourceRoot).relativeFilePath(resolvedSourcePath));
2638 if (sourceName.isEmpty()) {
2639 Log(tr("Compiled shader source cannot be resolved inside the active "
2640 "library: %1")
2641 .arg(sourcePath));
2642 return;
2643 }
2644
2645 const int shaderIndex = items.indexOf(sourceName, 0, Qt::CaseInsensitive);
2646 if (shaderIndex < 0) {
2647 Log(tr("Compiled shader is not present in the active library "
2648 "manifest: %1")
2649 .arg(sourceName));
2650 return;
2651 }
2652
2653 const QByteArray reloadPath = QFileInfo(runtimePath).canonicalFilePath().toUtf8();
2654 if (reloadPath.isEmpty()) {
2655 Log(tr("Compiled shader output cannot be resolved for live reload: %1").arg(runtimePath));
2656 return;
2657 }
2658 if (reloadPath.size() >= static_cast<int>(acmx2::ipc::kShaderSelectionMaxReloadPath)) {
2659 Log(tr("Compiled shader path is too long for live reload: %1").arg(runtimePath));
2660 return;
2661 }
2662
2663 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
2664 if (!lock) {
2665 Log(tr("Could not lock the live shader reload channel."));
2666 return;
2667 }
2668 shaderSelectionShm->reload_shader_index = shaderIndex;
2669 std::fill(std::begin(shaderSelectionShm->reload_shader_path), std::end(shaderSelectionShm->reload_shader_path), '\0');
2670 std::copy(reloadPath.cbegin(), reloadPath.cend(), shaderSelectionShm->reload_shader_path);
2671 ++shaderSelectionShm->reload_sequence;
2672 ++shaderSelectionShm->sequence;
2673 Log(tr("Requested live ACMXVK pipeline reload: %1<br>").arg(sourceName));
2674#else
2675 Q_UNUSED(sourcePath);
2676 Q_UNUSED(runtimePath);
2677#endif
2678}
2679
2681#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2682 if (!shaderSelectionShm)
2683 return;
2684
2685 std::array<qint32, acmx2::ipc::kShaderSelectionMaxPassCount> passIndices;
2686 passIndices.fill(-1);
2687 std::array<std::array<char, acmx2::ipc::kShaderSelectionMaxShaderName>, acmx2::ipc::kShaderSelectionMaxPassCount> passNames{};
2688
2689 quint32 passCount = 0;
2690 if (shader_pass_enabled && !shader_pass_names.isEmpty()) {
2691 loadShaders(shader_path, true);
2692 for (const QString &name : shader_pass_names) {
2694 break;
2695 const int idx = items.indexOf(name);
2696 if (idx < 0)
2697 continue;
2698 passIndices[passCount] = idx;
2699 const QByteArray shaderName = name.toUtf8();
2700 const qsizetype copyLength = std::min<qsizetype>(shaderName.size(), static_cast<qsizetype>(acmx2::ipc::kShaderSelectionMaxShaderName - 1));
2701 std::copy_n(shaderName.constData(), copyLength, passNames[passCount].begin());
2702 ++passCount;
2703 }
2704 }
2705
2706 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
2707 if (!lock) {
2708 Log("<br><style color=\"red\">Error lock failed</style><br>");
2709 return;
2710 }
2711 shaderSelectionShm->shader_pass_enabled = (shader_pass_enabled && passCount > 0) ? 1 : 0;
2712 shaderSelectionShm->shader_pass_count = passCount;
2713 std::copy(passIndices.begin(), passIndices.end(), std::begin(shaderSelectionShm->shader_pass_indices));
2714 for (std::size_t i = 0; i < passNames.size(); ++i) {
2715 std::copy(passNames[i].begin(), passNames[i].end(), shaderSelectionShm->shader_pass_names[i]);
2716 }
2717 ++shaderSelectionShm->sequence;
2718#endif
2719}
2720
2722#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2723 if (!shaderSelectionShm)
2724 return;
2725 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
2726 if (!lock) {
2727 Log("<br><style color=\"red\">Error lock failed</style><br>");
2728 return;
2729 }
2730 shaderSelectionShm->repeat_enabled = (play_repeat && play_repeat->isChecked()) ? 1 : 0;
2731 ++shaderSelectionShm->sequence;
2732#endif
2733}
2734
2736#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2737 if (!shaderSelectionShm)
2738 return;
2739
2740 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
2741 if (!lock) {
2742 Log("<br><style color=\"red\">Error lock failed</style><br>");
2743 return;
2744 }
2745 shaderSelectionShm->display_filter_enabled = display_filter_enabled ? 1 : 0;
2746 shaderSelectionShm->normalized_time_enabled = normalized_time ? 1 : 0;
2747
2748 const bool watermarkActive = watermark_enabled && !watermark_text.isEmpty();
2749 shaderSelectionShm->watermark_enabled = watermarkActive ? 1 : 0;
2750 shaderSelectionShm->watermark_r = static_cast<uint8_t>(std::clamp(watermark_r, 0, 255));
2751 shaderSelectionShm->watermark_g = static_cast<uint8_t>(std::clamp(watermark_g, 0, 255));
2752 shaderSelectionShm->watermark_b = static_cast<uint8_t>(std::clamp(watermark_b, 0, 255));
2753 std::fill(std::begin(shaderSelectionShm->watermark_text), std::end(shaderSelectionShm->watermark_text), '\0');
2754 const QByteArray wmUtf8 = watermark_text.toUtf8();
2755 const std::size_t wmCap = static_cast<std::size_t>(acmx2::ipc::kShaderSelectionMaxWatermarkText - 1);
2756 const std::size_t wmLen = std::min<std::size_t>(wmCap, static_cast<std::size_t>(wmUtf8.size()));
2757 std::copy_n(wmUtf8.constData(), static_cast<int>(wmLen), shaderSelectionShm->watermark_text);
2758
2759 std::array<qint32, acmx2::ipc::kShaderSelectionMaxGpuFilterCount> gpuIndices;
2760 gpuIndices.fill(-1);
2761 quint32 gpuCount = 0;
2763 const QStringList parts = gpu_filter_indices.split(',', Qt::SkipEmptyParts);
2764 for (const QString &part : parts) {
2766 break;
2767 bool ok = false;
2768 const int idx = part.trimmed().toInt(&ok);
2769 if (!ok || idx < 0)
2770 continue;
2771 gpuIndices[gpuCount++] = idx;
2772 }
2773 }
2774
2775 shaderSelectionShm->gpu_filter_enabled = (gpuCount > 0) ? 1 : 0;
2776 shaderSelectionShm->gpu_filter_count = gpuCount;
2777 shaderSelectionShm->gpu_buffer_size = static_cast<uint8_t>(std::clamp(gpu_buffer_size, 4, 32));
2778 std::copy(gpuIndices.begin(), gpuIndices.end(), std::begin(shaderSelectionShm->gpu_filter_indices));
2779
2780 const QByteArray dreamModel = deep_dream_model.toUtf8();
2781 const QByteArray dreamLayer = deep_dream_layer.toUtf8();
2782 const bool dreamStringsFit = dreamModel.size() < static_cast<int>(acmx2::ipc::kShaderSelectionMaxDreamModelPath) && dreamLayer.size() < static_cast<int>(acmx2::ipc::kShaderSelectionMaxDreamLayer);
2783 const bool dreamActive = active_backend == acmx2::Backend::Acmxvk && deep_dream_available && deep_dream_enabled && !dreamModel.isEmpty() && dreamStringsFit;
2784 shaderSelectionShm->dream_enabled = dreamActive ? 1 : 0;
2785 shaderSelectionShm->dream_fp16 = deep_dream_fp16 ? 1 : 0;
2786 shaderSelectionShm->dream_gpu_filter_first = deep_dream_gpu_filter_first ? 1 : 0;
2787 shaderSelectionShm->dream_iterations = deep_dream_iterations;
2788 shaderSelectionShm->dream_maximum_dimension = deep_dream_maximum_dimension;
2789 shaderSelectionShm->dream_channel = deep_dream_channel;
2790 shaderSelectionShm->dream_octaves = deep_dream_octaves;
2791 shaderSelectionShm->dream_jitter = deep_dream_jitter;
2792 shaderSelectionShm->dream_smoothing = deep_dream_smoothing;
2793 shaderSelectionShm->dream_strength = static_cast<float>(deep_dream_strength);
2794 shaderSelectionShm->dream_feedback = static_cast<float>(deep_dream_feedback);
2795 shaderSelectionShm->dream_zoom = static_cast<float>(deep_dream_zoom);
2796 shaderSelectionShm->dream_rotation = static_cast<float>(deep_dream_rotation);
2797 shaderSelectionShm->dream_octave_scale = static_cast<float>(deep_dream_octave_scale);
2798 std::fill(std::begin(shaderSelectionShm->dream_model_path), std::end(shaderSelectionShm->dream_model_path), '\0');
2799 std::fill(std::begin(shaderSelectionShm->dream_layer), std::end(shaderSelectionShm->dream_layer), '\0');
2800 if (dreamStringsFit) {
2801 std::copy(dreamModel.cbegin(), dreamModel.cend(), shaderSelectionShm->dream_model_path);
2802 std::copy(dreamLayer.cbegin(), dreamLayer.cend(), shaderSelectionShm->dream_layer);
2803 } else if (deep_dream_enabled) {
2804 Log("Deep Dream settings were not published because the model path "
2805 "or layer name is too long");
2806 }
2807
2808 ++shaderSelectionShm->sequence;
2809#endif
2810}
2811
2813#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2814 if (!shaderSelectionShm || !customUniformDialog)
2815 return;
2816
2817 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
2818 if (!lock) {
2819 Log("<br><style color=\"red\">Error lock failed</style><br>");
2820 return;
2821 }
2822 std::fill(&shaderSelectionShm->custom_uniform_names[0][0], &shaderSelectionShm->custom_uniform_names[0][0] + acmx2::ipc::kShaderSelectionMaxCustomUniforms * acmx2::ipc::kShaderSelectionMaxUniformName, '\0');
2823 std::fill(std::begin(shaderSelectionShm->custom_uniform_values), std::end(shaderSelectionShm->custom_uniform_values), 0.0f);
2824
2825 quint32 count = 0;
2828 break;
2829 const QByteArray name = uniform.name.toUtf8();
2830 if (name.isEmpty() || name.size() >= static_cast<int>(acmx2::ipc::kShaderSelectionMaxUniformName)) {
2831 continue;
2832 }
2833 std::copy(name.cbegin(), name.cend(), shaderSelectionShm->custom_uniform_names[count]);
2834 shaderSelectionShm->custom_uniform_values[count] = static_cast<float>(uniform.value);
2835 ++count;
2836 }
2837 shaderSelectionShm->custom_uniform_count = count;
2838 ++shaderSelectionShm->sequence;
2839#endif
2840}
2841
2843#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
2844 if (shaderSelectionShm) {
2845#if defined(__linux__) || defined(__APPLE__)
2846 ::munmap(shaderSelectionShm, sizeof(acmx2::ipc::ShaderSelectionShmData));
2847#else
2848 ::UnmapViewOfFile(shaderSelectionShm);
2849#endif
2850 shaderSelectionShm = nullptr;
2851 }
2852#if defined(__linux__) || defined(__APPLE__)
2853 if (shaderSelectionShmFd >= 0) {
2854 ::close(shaderSelectionShmFd);
2855 shaderSelectionShmFd = -1;
2856 }
2857#else
2858 if (shaderSelectionMapping != nullptr) {
2859 ::CloseHandle(shaderSelectionMapping);
2860 shaderSelectionMapping = nullptr;
2861 }
2862#endif
2864#endif
2865}
2866
2868#if defined(__linux__) || defined(__APPLE__)
2869 if (shaderSelectionSemaphore == SEM_FAILED)
2870 return;
2871
2872 //::sem_unlink(acmx2::ipc::kShaderSelectionSemaphoreName);
2873 ::sem_close(shaderSelectionSemaphore);
2874 shaderSelectionSemaphore = SEM_FAILED;
2875#elif defined(_WIN32)
2876 if (shaderSelectionSemaphore == nullptr)
2877 return;
2878
2879 ::CloseHandle(shaderSelectionSemaphore);
2880 shaderSelectionSemaphore = nullptr;
2881#endif
2882}
2883
2885 if (!list_view || row < 0 || row >= list_view->topLevelItemCount())
2886 return;
2887 QTreeWidgetItem *it = list_view->topLevelItem(row);
2888 if (!it)
2889 return;
2890 list_view->setCurrentItem(it);
2891 list_view->scrollToItem(it, QAbstractItemView::PositionAtCenter);
2892}
2893
2895 shaderCacheStatus.clear();
2896 shaderCacheMTime = QDateTime();
2898 return;
2899#ifdef Q_OS_MACOS
2900 // There is no persistent binary cache to inspect on macOS. Source saves
2901 // are handled by the live-reload IPC path instead.
2902 return;
2903#else
2904 if (shader_path.isEmpty())
2905 return;
2906 const QString cachePath = resolveShaderCachePath(shader_path, cache_size, cache_enabled && textureCacheArraySettingEnabled());
2907 QFileInfo cacheInfo(cachePath);
2908 if (!cacheInfo.exists() || !cacheInfo.isFile()) {
2909 Log("Shader cache not found at: " + cachePath);
2910 return;
2911 }
2912 shaderCacheMTime = cacheInfo.lastModified();
2913 shaderCacheStatus = parseShaderCacheStatus(cachePath);
2914 // Log("Shader cache: " + cachePath + " (" + QString::number(shaderCacheStatus.size()) + " entries)");
2915#endif
2916}
2917
2919 if (!list_view)
2920 return;
2922
2923 // Preserve the currently selected row so a refresh (e.g. after the
2924 // child process exits) does not lose the user's place in the list.
2925 const int previousRow = currentShaderRow();
2926
2927 const QSignalBlocker blocker(list_view);
2928 list_view->clear();
2929
2930 QString acmxvk_type_error;
2931 const bool acmxvk_source = active_backend == acmx2::Backend::Acmxvk && is_acmxvk_source_library(shader_path, acmxvk_type_error) && acmxvk_type_error.isEmpty();
2932 const int width = QString::number(items.size()).size();
2933 for (int i = 0; i < items.size(); ++i) {
2934 const QString &name = items.at(i);
2935 QFileInfo fi(shader_path + "/" + name);
2936 const QString stem = QFileInfo(name).completeBaseName();
2937 const bool isCompute = name.endsWith(QStringLiteral(".comp"), Qt::CaseInsensitive) || name.endsWith(QStringLiteral(".comp.spv"), Qt::CaseInsensitive);
2938 const QString shaderType = isCompute ? tr("Compute") : tr("Fragment");
2939
2940 QString health;
2941 QColor healthColor;
2943 const AcmxvkBuildState state = acmxvk_source ? acmxvk_shader_build_state(shader_path, name) : AcmxvkBuildState::UpToDate;
2944 if (state == AcmxvkBuildState::NotBuilt) {
2945 health = tr("Not Built");
2946 healthColor = QColor("#888888");
2947 } else if (state == AcmxvkBuildState::Stale) {
2948 health = tr("Stale");
2949 healthColor = QColor("#ffaa00");
2950 } else {
2951 health = tr("Up to Date");
2952 healthColor = QColor("#55ff55");
2953 }
2954 } else if (shaderCacheStatus.isEmpty()) {
2955 health = tr("No cache");
2956 healthColor = QColor("#888888");
2957 } else if (!shaderCacheStatus.contains(stem)) {
2958 health = tr("Uncached");
2959 healthColor = QColor("#cccc00");
2960 } else if (shaderCacheStatus.value(stem)) {
2961 health = tr("Failed");
2962 healthColor = QColor("#ff5555");
2963 } else if (fi.exists() && shaderCacheMTime.isValid() && fi.lastModified() > shaderCacheMTime) {
2964 health = tr("Stale");
2965 healthColor = QColor("#ffaa00");
2966 } else {
2967 health = tr("Cached");
2968 healthColor = QColor("#55ff55");
2969 }
2970
2971 QStringList cols;
2972 cols << QString("%1").arg(i, width, 10, QLatin1Char(' ')) << name << (fi.exists() ? formatLastModified(fi.lastModified()) : tr("missing")) << health << shaderType;
2973 auto *item = new QTreeWidgetItem(list_view, cols);
2974 item->setTextAlignment(0, Qt::AlignRight | Qt::AlignVCenter);
2975 item->setForeground(3, QBrush(healthColor));
2976 if (!fi.exists())
2977 item->setForeground(2, QBrush(QColor("#ff5555")));
2978 }
2979
2980 // Restore the previously selected row after the rebuild.
2981 if (previousRow >= 0 && previousRow < list_view->topLevelItemCount()) {
2982 QTreeWidgetItem *it = list_view->topLevelItem(previousRow);
2983 if (it) {
2984 list_view->setCurrentItem(it);
2985 list_view->scrollToItem(it, QAbstractItemView::PositionAtCenter);
2986 }
2987 }
2988}
2989
2990void MainWindow::Log(const QString &message) {
2991 QString normalized = message;
2992 while (normalized.endsWith('\n') || normalized.endsWith('\r')) {
2993 normalized.chop(1);
2994 }
2995
2996 bottomTextBox->append(normalized);
2997 QTextCursor cursor = bottomTextBox->textCursor();
2998 cursor.movePosition(QTextCursor::End);
2999 bottomTextBox->setTextCursor(cursor);
3000}
3001
3002void MainWindow::Write(const QString &message) {
3003 QTextCursor cursor = bottomTextBox->textCursor();
3004 cursor.movePosition(QTextCursor::End);
3005 cursor.insertHtml(message);
3006 bottomTextBox->setTextCursor(cursor);
3007
3008 constexpr int MAX_BLOCKS = 5000;
3009 QTextDocument *doc = bottomTextBox->document();
3010 int excess = doc->blockCount() - MAX_BLOCKS;
3011 if (excess > 0) {
3012 QTextCursor trim(doc);
3013 trim.movePosition(QTextCursor::Start);
3014 trim.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor, excess);
3015 trim.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
3016 trim.removeSelectedText();
3017 trim.deleteChar();
3018 }
3019}
3020
3021void MainWindow::beginOutputRunLog(const QString &command) {
3022 if (!save_output_log || output_file.isEmpty()) {
3023 return;
3024 }
3025
3026 if (output_run_log.isOpen()) {
3027 output_run_log.close();
3028 }
3029 output_run_log.setFileName(output_file + QStringLiteral(".log"));
3030 if (!output_run_log.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
3031 Log(tr("<b style='color:red;'>Unable to save render log: %1</b>").arg(output_run_log.errorString()));
3032 return;
3033 }
3034
3035 appendOutputRunLog(tr("ACMX render log"));
3036 appendOutputRunLog(tr("Started: %1").arg(QDateTime::currentDateTime().toString(Qt::ISODate)));
3037 appendOutputRunLog(tr("Command: %1").arg(command));
3038 appendOutputRunLog(QString());
3039}
3040
3041void MainWindow::appendOutputRunLog(const QString &message) {
3042 if (!output_run_log.isOpen()) {
3043 return;
3044 }
3045
3046 output_run_log.write(message.toUtf8());
3047 if (!message.endsWith(QLatin1Char('\n'))) {
3048 output_run_log.write("\n");
3049 }
3050 output_run_log.flush();
3051}
3052
3053void MainWindow::finishOutputRunLog(int exitCode, QProcess::ExitStatus exitStatus) {
3054 if (!output_run_log.isOpen()) {
3055 return;
3056 }
3057
3058 appendOutputRunLog(QString());
3059 appendOutputRunLog(tr("Finished: %1").arg(QDateTime::currentDateTime().toString(Qt::ISODate)));
3060 appendOutputRunLog(tr("Exit: %1 (%2)").arg(exitCode).arg(exitStatus == QProcess::NormalExit ? tr("normal") : tr("crashed")));
3061 output_run_log.close();
3062}
3063
3065 QSettings settings("LostSideDead");
3066 QString startDirectory = settings.value("lastShaderDir").toString();
3067 if (startDirectory.isEmpty())
3068 startDirectory = shader_path;
3069 if (startDirectory.isEmpty())
3070 startDirectory = QDir::homePath();
3071
3072 const QString directory = QFileDialog::getExistingDirectory(this, tr("Load Shader Library"), startDirectory, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
3073 if (directory.isEmpty())
3074 return;
3075
3076 settings.setValue("lastShaderDir", directory);
3077 loadLibraryPath(directory);
3078}
3079
3080bool MainWindow::backend_launch_available() const { return true; }
3081
3082void MainWindow::prompt_acmxvk_rebuild(const QString &reason, PendingAcmxvkAction resume_action) {
3083 QString type_error;
3084 if (!is_acmxvk_source_library(shader_path, type_error) || !type_error.isEmpty()) {
3085 QMessageBox::warning(this, tr("Build ACMXVK Library"), reason);
3086 return;
3087 }
3088
3089 const QMessageBox::StandardButton answer = QMessageBox::question(this,
3090 tr("Build ACMXVK Library"),
3091 tr("The ACMXVK build is out of date or incomplete.\n\n%1\n\n"
3092 "Do you wish to rebuild it now?")
3093 .arg(reason),
3094 QMessageBox::Yes | QMessageBox::No,
3095 QMessageBox::Yes);
3096 if (answer != QMessageBox::Yes)
3097 return;
3098
3099 pending_acmxvk_action = resume_action;
3100 Log(tr("ACMXVK rebuild requested before launch."));
3102}
3103
3105 const QString name = acmx2::backend_name(active_backend);
3106 setWindowTitle(tr("%1 - Interface").arg(name));
3111
3112 const bool launchAvailable = backend_launch_available();
3113 const bool acmx2Tools = active_backend == acmx2::Backend::Acmx2;
3114 if (deepDreamAction) {
3115 deepDreamAction->setVisible(!acmx2Tools);
3116 }
3117 QString sourceTypeError;
3118 const bool acmxvkSource = active_backend == acmx2::Backend::Acmxvk && !shader_path.isEmpty() && is_acmxvk_source_library(shader_path, sourceTypeError) && sourceTypeError.isEmpty();
3119 if (runMenu_select)
3120 runMenu_select->setEnabled(launchAvailable);
3121 if (runMenu_all)
3122 runMenu_all->setEnabled(launchAvailable);
3124 runMenu_copyCommand->setEnabled(launchAvailable);
3125 if (buildCacheAction) {
3126 buildCacheAction->setText(acmxvkSource ? tr("Build") : tr("Rebuild Shader Cache"));
3127 buildCacheAction->setVisible(acmx2Tools || acmxvkSource);
3128 buildCacheAction->setEnabled(acmx2Tools || acmxvkSource);
3129 buildCacheAction->setToolTip(acmxvkSource ? tr("Compile changed GLSL sources into %1").arg(acmxvk_build_directory(shader_path)) : QString());
3130 }
3131 if (fixBuildAction) {
3132 fixBuildAction->setVisible(acmxvkSource);
3133 fixBuildAction->setEnabled(acmxvkSource);
3134 fixBuildAction->setToolTip(acmxvkSource ? tr("Build into %1 and omit shaders that fail to compile").arg(acmxvk_build_directory(shader_path)) : QString());
3135 }
3137 cleanShaderCacheAction->setVisible(acmx2Tools);
3138 cleanShaderCacheAction->setEnabled(acmx2Tools);
3139 }
3140 if (removeBrokenAction) {
3141 removeBrokenAction->setVisible(acmx2Tools || acmxvkSource);
3142 removeBrokenAction->setEnabled(acmx2Tools || acmxvkSource);
3143 removeBrokenAction->setToolTip(acmxvkSource ? tr("Permanently delete .frag and .comp sources that fail "
3144 "the ACMXVK Fix Build")
3145 : QString());
3146 }
3147 if (runFromCacheAction) {
3148 runFromCacheAction->setVisible(acmx2Tools);
3149 runFromCacheAction->setEnabled(acmx2Tools);
3150 }
3151#ifdef Q_OS_MACOS
3152 if (buildCacheAction && acmx2Tools) {
3153 buildCacheAction->setVisible(false);
3154 buildCacheAction->setEnabled(false);
3155 }
3157 cleanShaderCacheAction->setEnabled(false);
3159 runFromCacheAction->setEnabled(false);
3160#endif
3161 const bool processIdle = !process || process->state() == QProcess::NotRunning;
3163 libraryBuilderAction->setEnabled(processIdle);
3164 if (listMenu_new)
3165 listMenu_new->setEnabled(processIdle);
3166 if (listMenu_shader)
3167 listMenu_shader->setEnabled(processIdle);
3168 if (list_view) {
3169#ifdef Q_OS_MACOS
3170 list_view->setColumnHidden(3, acmx2Tools);
3171#else
3172 list_view->setColumnHidden(3, false);
3173#endif
3174 list_view->headerItem()->setText(3, acmx2Tools ? tr("Compile Health") : tr("Build Status"));
3175 }
3176
3177 if (runMenu) {
3178 runMenu_select->setToolTip({});
3179 runMenu_all->setToolTip({});
3180 runMenu_copyCommand->setToolTip({});
3181 }
3182 if (list_view) {
3183 list_view->setToolTip(tr("Right click while running to change the active shader."));
3184 }
3185}
3186
3187void MainWindow::set_backend(acmx2::Backend backend, bool persist) {
3188 if (process && process->state() == QProcess::Running) {
3189 QMessageBox::information(this, tr("Process Running"), tr("Stop the running process before changing backends."));
3191 return;
3192 }
3193
3195 libraryBuilderDialog->close();
3196 libraryBuilderDialog = nullptr;
3197 }
3199 deepDreamSettingsDialog->close();
3200 deepDreamSettingsDialog = nullptr;
3201 }
3202 if (gpuFilterDialog) {
3203 gpuFilterDialog->close();
3204 gpuFilterDialog = nullptr;
3205 }
3206
3207 QSettings settings("LostSideDead");
3208 settings.setValue(acmx2::backend_settings_key(active_backend, "executable"), executable_path);
3209 settings.setValue(acmx2::backend_settings_key(active_backend, "library"), shader_path);
3210
3211 active_backend = backend;
3212 if (persist)
3213 settings.setValue("interface/backend", acmx2::backend_id(active_backend));
3215 const QString nextLibrary = settings.value(acmx2::backend_settings_key(active_backend, "library"), "").toString().trimmed();
3216
3217 shader_path.clear();
3218 items.clear();
3219 indexTimestamp = QDateTime();
3221 if (list_view)
3222 list_view->clear();
3223
3224 if (!nextLibrary.isEmpty() && QFileInfo(nextLibrary).isDir() && acmx2::shader_manifest_exists(nextLibrary)) {
3225 QString backendError;
3226 const std::optional<acmx2::Backend> libraryBackend = acmx2::shader_manifest_backend(nextLibrary, backendError);
3227 if (!backendError.isEmpty()) {
3228 Log(tr("Warning: Could not read backend metadata for %1: %2").arg(nextLibrary, backendError));
3229 } else if (libraryBackend && *libraryBackend != active_backend) {
3230 Log(tr("Warning: Saved %1 library belongs to %2: %3").arg(acmx2::backend_name(active_backend), acmx2::backend_name(*libraryBackend), nextLibrary));
3231 } else {
3232 shader_path = nextLibrary;
3233 loadShaders(shader_path, true);
3234 }
3235 }
3236
3237 cuda_available = false;
3238 cuda_device_available = false;
3239 audio_available = false;
3240 midi_available = false;
3241 dnn_available = false;
3242 deep_dream_available = false;
3248 settings.sync();
3249 Log(tr("Backend selected: %1").arg(acmx2::backend_name(active_backend)));
3251 Log(tr("ACMXVK launching and live shader selection are enabled."));
3252}
3253
3254bool MainWindow::loadLibraryPath(const QString &path) {
3255 const QString trimmedPath = path.trimmed();
3256 if (trimmedPath.isEmpty())
3257 return false;
3258 const QString libraryPath = QDir::cleanPath(trimmedPath);
3259 const QFileInfo libraryInfo(libraryPath);
3260 if (!libraryInfo.exists()) {
3261 QMessageBox::warning(this, tr("Invalid Shader Path"), tr("Shader directory does not exist:\n%1").arg(libraryPath));
3262 return false;
3263 }
3264 if (!libraryInfo.isDir()) {
3265 QMessageBox::warning(this, tr("Invalid Shader Path"), tr("Shader path is not a directory:\n%1").arg(libraryPath));
3266 return false;
3267 }
3268 if (!acmx2::shader_manifest_exists(libraryPath)) {
3269 QMessageBox::warning(this, tr("Missing Shader Manifest"), tr("Shader directory does not contain library.json or index.txt:\n%1").arg(libraryPath));
3270 return false;
3271 }
3272 QString backendError;
3273 const std::optional<acmx2::Backend> libraryBackend = acmx2::shader_manifest_backend(libraryPath, backendError);
3274 if (!backendError.isEmpty()) {
3275 QMessageBox::warning(this, tr("Invalid Backend Metadata"), backendError);
3276 return false;
3277 }
3278 if (libraryBackend && *libraryBackend != active_backend) {
3279 const QMessageBox::StandardButton reply = QMessageBox::question(this,
3280 tr("Switch Backend"),
3281 tr("This library targets %1, but the active backend is %2.\n\n"
3282 "Switch to %1 and load it?")
3284 QMessageBox::Yes | QMessageBox::No,
3285 QMessageBox::Yes);
3286 if (reply != QMessageBox::Yes)
3287 return false;
3288 set_backend(*libraryBackend);
3289 }
3291 QString libraryTypeError;
3292 acmx2::shader_manifest_library_type(libraryPath, libraryTypeError);
3293 if (!libraryTypeError.isEmpty()) {
3294 QMessageBox::warning(this, tr("Invalid Library Type"), libraryTypeError);
3295 return false;
3296 }
3297 }
3298 if (!loadShaders(libraryPath, true)) {
3299 Log(tr("Warning: Could not load shaders from directory: %1").arg(libraryPath));
3300 return false;
3301 }
3302
3303 QSettings settings("LostSideDead");
3304 settings.setValue(acmx2::backend_settings_key(active_backend, "library"), libraryPath);
3306 settings.setValue("shaders", libraryPath);
3307 settings.sync();
3308 addRecentLibrary(libraryPath);
3309 Log(tr("Successfully loaded shader library: %1").arg(libraryPath));
3311 return true;
3312}
3313
3314void MainWindow::addRecentLibrary(const QString &path) {
3315 const QString trimmedPath = path.trimmed();
3316 if (trimmedPath.isEmpty())
3317 return;
3318 const QString libraryPath = QDir::cleanPath(trimmedPath);
3319
3320 QSettings settings("LostSideDead");
3321 const QString recentKey = acmx2::backend_settings_key(active_backend, "recentLibraries");
3322 const QStringList legacyRecent = active_backend == acmx2::Backend::Acmx2 ? settings.value("recentLibraries").toStringList() : QStringList();
3323 QStringList recentLibraries = settings.value(recentKey, legacyRecent).toStringList();
3324 for (auto it = recentLibraries.begin(); it != recentLibraries.end();) {
3325 if (QDir::cleanPath(*it).compare(libraryPath, Qt::CaseInsensitive) == 0)
3326 it = recentLibraries.erase(it);
3327 else
3328 ++it;
3329 }
3330 recentLibraries.prepend(libraryPath);
3331 while (recentLibraries.size() > RECENT_LIBRARY_LIMIT)
3332 recentLibraries.removeLast();
3333 settings.setValue(recentKey, recentLibraries);
3335 settings.setValue("recentLibraries", recentLibraries);
3336 settings.sync();
3338}
3339
3341 if (!loadRecentMenu)
3342 return;
3343
3344 loadRecentMenu->clear();
3345 QSettings settings("LostSideDead");
3346 const QString recentKey = acmx2::backend_settings_key(active_backend, "recentLibraries");
3347 const QStringList legacyRecent = active_backend == acmx2::Backend::Acmx2 ? settings.value("recentLibraries").toStringList() : QStringList();
3348 const QStringList recentLibraries = settings.value(recentKey, legacyRecent).toStringList();
3349 if (recentLibraries.isEmpty()) {
3350 QAction *emptyAction = loadRecentMenu->addAction(tr("No Recent Libraries"));
3351 emptyAction->setEnabled(false);
3352 return;
3353 }
3354
3355 for (const QString &path : recentLibraries) {
3356 QAction *action = loadRecentMenu->addAction(path);
3357 connect(action, &QAction::triggered, this, [this, path]() { loadLibraryPath(path); });
3358 }
3359}
3360
3362 const acmx2::Backend propertiesBackend = active_backend;
3363 PropWindow propWindow(propertiesBackend, this);
3364 if (propWindow.exec() == QDialog::Accepted) {
3365 QString exePath = propWindow.exePathLineEdit->text();
3366 QString shaderDir = propWindow.shaderDirLineEdit->text();
3367 QString prefix = propWindow.screenshotDirLineEdit->text();
3368 QString compilerMode;
3369 QString compilerPath;
3370 if (propertiesBackend == acmx2::Backend::Acmxvk && propWindow.shaderCompilerComboBox) {
3371 compilerMode = propWindow.shaderCompilerComboBox->currentData().toString();
3372 compilerPath = propWindow.shaderCompilerPathLineEdit->text().trimmed();
3373 if (compilerMode == QStringLiteral("custom") && compilerPath.isEmpty()) {
3374 QMessageBox::information(this, tr("Shader Compiler"), tr("Select a custom glslc-compatible compiler path."));
3375 return;
3376 }
3377 }
3378 if (exePath.length() == 0) {
3379 QMessageBox::information(this, "No Path", "Requires Executable path");
3380 return;
3381 }
3382 if (shaderDir.length() == 0) {
3383 QMessageBox::information(this, "Shader Path", "Requires Shader Path");
3384 return;
3385 }
3386
3387 if (!loadLibraryPath(shaderDir))
3388 return;
3389
3390 QSettings appSettings("LostSideDead");
3391 if (active_backend == propertiesBackend) {
3392 appSettings.setValue(acmx2::backend_settings_key(active_backend, "executable"), exePath);
3394 appSettings.setValue("exePath", exePath);
3395 executable_path = exePath;
3396 if (propertiesBackend == acmx2::Backend::Acmxvk) {
3397 appSettings.setValue(acmx2::backend_settings_key(acmx2::Backend::Acmxvk, "shader_compiler_mode"), compilerMode.isEmpty() ? QStringLiteral("auto") : compilerMode);
3398 appSettings.setValue(acmx2::backend_settings_key(acmx2::Backend::Acmxvk, "shader_compiler_path"), compilerPath);
3399 }
3400 } else {
3401 Log(tr("Backend changed while loading the library; retained the "
3402 "%1 executable setting.")
3404 }
3405 appSettings.setValue("prefix_path", prefix);
3406 appSettings.sync();
3407
3408 prefix_path = prefix;
3409
3410 Log("Executable Path: " + executable_path);
3411 Log("Prefix Path: " + prefix);
3412 Log("Shader Directory: " + shaderDir);
3413
3414 } else {
3415 Log("Canceled");
3416 }
3417}
3418
3420 if (!customUniformDialog || shader_path.isEmpty()) {
3421 QMessageBox::information(this, tr("Custom Uniforms"), tr("Load a shader library first."));
3422 return;
3423 }
3424 const QString jsonPath = QDir(shader_path).filePath("library.json");
3425 if (!QFileInfo(jsonPath).isFile()) {
3426 QMessageBox::warning(this, tr("Custom Uniforms"), tr("Custom uniforms require a library.json manifest."));
3427 return;
3428 }
3429
3430 QString error;
3432 QMessageBox::warning(this, tr("Could Not Load Custom Uniforms"), error);
3433 return;
3434 }
3435 customUniformDialog->show();
3436 customUniformDialog->raise();
3437 customUniformDialog->activateWindow();
3438}
3439
3443 uniformReferenceDialog->setAttribute(Qt::WA_DeleteOnClose);
3444 } else
3446 uniformReferenceDialog->show();
3447 uniformReferenceDialog->raise();
3448 uniformReferenceDialog->activateWindow();
3449}
3450
3451bool MainWindow::loadShaders(const QString &path, bool force) {
3452 QString manifestPath = acmx2::shader_manifest_path(path);
3453 if (manifestPath.isEmpty()) {
3454 QMessageBox::warning(this, "Could not open shader manifest", "No library.json or index.txt found in: " + path);
3455 return false;
3456 }
3457
3458 if (active_backend == acmx2::Backend::Acmx2 && QFileInfo(manifestPath).fileName().compare("index.txt", Qt::CaseInsensitive) == 0) {
3459 bool generated = false;
3460 QString migrationError;
3461 if (!acmx2::migrate_index_manifest_to_json(path, generated, migrationError)) {
3462 Log("Could not generate library.json from index.txt: " + migrationError);
3463 } else if (generated) {
3464 manifestPath = acmx2::shader_manifest_path(path);
3465 Log("Generated library.json from index.txt");
3466 }
3467 }
3468
3469 QDateTime modified = QFileInfo(manifestPath).lastModified();
3470 if (!force && path == shader_path && manifestPath == activeShaderManifestPath && !indexTimestamp.isNull() && modified <= indexTimestamp) {
3471 return true;
3472 }
3473 QStringList manifestEntries;
3474 QString manifestError;
3475 if (!acmx2::load_shader_manifest(path, manifestEntries, manifestError)) {
3476 QMessageBox::warning(this, "Could not open shader manifest", manifestError);
3477 return false;
3478 }
3479
3480 shader_path = path;
3481 activeShaderManifestPath = manifestPath;
3482 indexTimestamp = modified;
3483 if (customUniformDialog && QFileInfo(manifestPath).fileName().compare("library.json", Qt::CaseInsensitive) == 0) {
3484 QString uniformError;
3485 if (!customUniformDialog->loadLibrary(path, active_backend, &uniformError))
3486 Log("Could not load custom uniforms: " + uniformError);
3487 }
3489 const int previousRow = currentShaderRow();
3490 const QString previouslySelected = currentShaderName();
3491 items.clear();
3492 QStringList uniqueItems;
3493 for (const QString &rawEntry : manifestEntries) {
3494 const QString line = rawEntry.trimmed();
3495
3496 if (line.isEmpty()) {
3497 continue;
3498 }
3499 const QString shaderEntry = sanitizeShaderName(line);
3500 if (shaderEntry.isEmpty()) {
3501 Log("Skipping invalid shader path in " + QFileInfo(manifestPath).fileName() + ": " + line);
3502 continue;
3503 }
3504 QString fullPath = path + "/" + shaderEntry;
3505 QFileInfo fileInfo(fullPath);
3506 if (!fileInfo.exists() || !fileInfo.isFile()) {
3507 Log("Skipping non-existent file: " + shaderEntry);
3508 continue;
3509 }
3510 if (!uniqueItems.contains(shaderEntry, Qt::CaseInsensitive)) {
3511 uniqueItems.append(shaderEntry);
3512 } else {
3513 Log("Skipping duplicate shader: " + shaderEntry);
3514 }
3515 }
3516 items = uniqueItems;
3517
3518 Log("Loaded " + QString::number(items.size()) + " unique shader files");
3520 menuSort();
3521
3522 if (!items.isEmpty()) {
3523 int restoredRow = previousRow;
3524 if (restoredRow < 0 || restoredRow >= items.size()) {
3525 if (!previouslySelected.isEmpty() && items.contains(previouslySelected)) {
3526 restoredRow = items.indexOf(previouslySelected);
3527 } else {
3528 restoredRow = 0;
3529 }
3530 }
3531 selectShaderRow(restoredRow);
3532 }
3533
3534 return true;
3535}
3536
3537void MainWindow::fileExit() { QApplication::quit(); }
3538
3540 if (!audio_available) {
3541 QMessageBox::information(this, tr("Audio Settings"), tr("Audio support is unavailable: acmx2 was built without audio support."));
3542 return;
3543 }
3544 const QString previousAudioFile = audio_file;
3545 const int previousAudioOutput = audio_output;
3546 const bool previousAudioPassThrough = audio_passthrough;
3547 const bool previousAudioTrunc = audio_trunc;
3548 const bool previousAudioRepeat = audio_repeat;
3549 AudioSettings audio_set(this);
3550 if (audio_set.exec() == QDialog::Accepted) {
3552 audio_channels = audio_set.getNumberOfChannels();
3553 audio_sense = audio_set.getSensitivity();
3555 record_audio = audio_set.isRecordAudioEnabled();
3556 record_volume = audio_set.getRecordVolume();
3557 audio_input = audio_set.getInputDeviceIndex();
3558 audio_output = audio_set.getOutputDeviceIndex();
3559 if (audio_set.isAudioFileEnabled()) {
3560 audio_file = audio_set.getAudioFilePath();
3561 } else {
3562 audio_file = "";
3563 }
3564 audio_trunc = audio_set.isAudioTruncEnabled();
3565 audio_repeat = audio_set.isAudioRepeatEnabled();
3568 audio_warm_rate = audio_set.getAudioWarmRate();
3569 Log("Audio Settings Saved");
3570#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
3571 const bool liveAudioSettingsChanged = QFileInfo(audio_file).absoluteFilePath() != QFileInfo(previousAudioFile).absoluteFilePath() || audio_output != previousAudioOutput || audio_passthrough != previousAudioPassThrough || audio_trunc != previousAudioTrunc || audio_repeat != previousAudioRepeat;
3572 if (shaderSelectionShm && process && process->state() == QProcess::Running && !audio_file.isEmpty() && liveAudioSettingsChanged) {
3573 const QByteArray path = QFileInfo(audio_file).absoluteFilePath().toUtf8();
3574 if (path.size() >= static_cast<int>(acmx2::ipc::kShaderSelectionMaxAudioFilePath)) {
3575 Log("Audio file path is too long for live playback: " + audio_file);
3576 } else {
3577 acmx2::ipc::ShaderSelectionLock lock(shaderSelectionSemaphore);
3578 if (!lock) {
3579 Log("Could not lock the live playback control channel");
3580 return;
3581 }
3582 std::fill(std::begin(shaderSelectionShm->audio_file_path), std::end(shaderSelectionShm->audio_file_path), '\0');
3583 std::copy(path.cbegin(), path.cend(), shaderSelectionShm->audio_file_path);
3584 shaderSelectionShm->audio_output_device = audio_output;
3585 shaderSelectionShm->audio_pass_through = audio_passthrough ? 1 : 0;
3586 shaderSelectionShm->audio_trunc = audio_trunc ? 1 : 0;
3587 shaderSelectionShm->audio_repeat = audio_repeat ? 1 : 0;
3588 ++shaderSelectionShm->audio_file_sequence;
3589 ++shaderSelectionShm->sequence;
3590 Log("Requested live audio-file change: " + audio_file + "<br>");
3591 }
3592 }
3593#endif
3594 }
3595}
3596
3598 if (!cuda_available) {
3599 QMessageBox::information(this, tr("GPU Filter Settings"), tr("GPU filters are unavailable: acmx2 was built without CUDA support."));
3600 return;
3601 }
3602
3603 if (gpuFilterDialog) {
3604 gpuFilterDialog->show();
3605 gpuFilterDialog->raise();
3606 gpuFilterDialog->activateWindow();
3607 return;
3608 }
3609
3611 gpuFilterDialog->setAttribute(Qt::WA_DeleteOnClose);
3613
3614 auto applyGpuDialogSettings = [this](bool enabled, const QString &filters, int bufferSize) {
3615 gpu_filter_enabled = enabled;
3616 gpu_filter_indices = filters;
3617 gpu_buffer_size = bufferSize;
3620 QSettings("LostSideDead", "acmx2").setValue("deep_dream/gpu_filter_first", false);
3621 Log("Deep Dream pipeline order reset because GPU filtering was disabled");
3622 }
3623 if (gpu_filter_enabled) {
3624 Log("GPU Filter Settings Saved: Filters=" + gpu_filter_indices + ", Buffer=" + QString::number(gpu_buffer_size));
3625 } else {
3626 Log("GPU Filtering Disabled");
3627 }
3629 };
3630
3631 connect(dialog, &GPUFilterDialog::settingsApplied, this, [applyGpuDialogSettings](bool enabled, const QString &filterArgument, int bufferSize) { applyGpuDialogSettings(enabled, filterArgument, bufferSize); });
3632 connect(dialog, &QDialog::accepted, this, [dialog, applyGpuDialogSettings]() { applyGpuDialogSettings(dialog->isGPUFilterEnabled(), dialog->getFilterArgument(), dialog->getBufferSize()); });
3633
3634 dialog->show();
3635 dialog->raise();
3636 dialog->activateWindow();
3637}
3638
3641 QMessageBox::information(this,
3642 tr("Deep Dream Settings"),
3643 tr("Deep Dream is unavailable: ACMXVK must be built with "
3644 "-DWITH_DEEP_DREAM=ON and CUDA-enabled LibTorch."));
3645 return;
3646 }
3647
3650 deepDreamSettingsDialog->raise();
3651 deepDreamSettingsDialog->activateWindow();
3652 return;
3653 }
3654
3655 const bool gpu_filter_configured = cuda_available && gpu_filter_enabled && !gpu_filter_indices.trimmed().isEmpty();
3656 deepDreamSettingsDialog = new DeepDreamSettingsDialog(gpu_filter_configured, this);
3657 deepDreamSettingsDialog->setAttribute(Qt::WA_DeleteOnClose);
3659 connect(dialog, &DeepDreamSettingsDialog::settingsApplied, this, [this, dialog]() {
3660 const DeepDreamConfiguration config = dialog->configuration();
3661 deep_dream_enabled = config.enabled;
3663 deep_dream_layer = config.layer;
3667 deep_dream_zoom = config.zoom;
3670 deep_dream_fp16 = config.fp16;
3671 deep_dream_channel = config.channel;
3672 deep_dream_octaves = config.octaves;
3674 deep_dream_jitter = config.jitter;
3679
3680 if (deep_dream_enabled) {
3681 Log(tr("Deep Dream Settings Applied: %1/%2, %3 "
3682 "iteration(s), rotation %4 degrees, %5, %6")
3683 .arg(QFileInfo(deep_dream_model).fileName(), deep_dream_layer)
3685 .arg(deep_dream_rotation, 0, 'f', 3)
3686 .arg(deep_dream_gpu_filter_first ? tr("acidcam-gpu first") : tr("Deep Dream first"))
3687 .arg(deep_dream_original ? tr("independent-frame preview") : tr("temporal feedback")));
3688 } else {
3689 Log("Deep Dream Disabled");
3690 }
3691 });
3692 dialog->show();
3693 dialog->raise();
3694 dialog->activateWindow();
3695}
3696
3697bool MainWindow::validateDeepDreamLaunch(QString &error) const {
3698 error.clear();
3700 return true;
3701 }
3702 if (!deep_dream_available) {
3703 error = tr("The selected ACMXVK executable does not provide Deep "
3704 "Dream support.");
3705 return false;
3706 }
3707 if (!QFileInfo(deep_dream_model).isFile()) {
3708 error = tr("The configured Deep Dream model does not exist:\n%1").arg(deep_dream_model);
3709 return false;
3710 }
3711 if (deep_dream_layer.trimmed().isEmpty()) {
3712 error = tr("Select a Deep Dream feature layer.");
3713 return false;
3714 }
3715 if (deep_dream_original && (video_file.isEmpty() || output_file.isEmpty())) {
3716 error = tr("Independent-frame preview requires video input and an "
3717 "enabled video output file.");
3718 return false;
3719 }
3721 return true;
3722 }
3723 if (!cuda_available || !gpu_filter_enabled || gpu_filter_indices.trimmed().isEmpty()) {
3724 error = tr("Running acidcam-gpu before Deep Dream requires an enabled "
3725 "GPU filter chain.");
3726 return false;
3727 }
3728 if (!graphics_file.isEmpty()) {
3729 error = tr("Running acidcam-gpu before Deep Dream currently supports "
3730 "camera and video input, not still graphics.");
3731 return false;
3732 }
3733 if (maximize_fps) {
3734 error = tr("Running acidcam-gpu before Deep Dream cannot be combined "
3735 "with Maximize FPS.");
3736 return false;
3737 }
3738 if (onnx_model_enabled && !onnx_model.isEmpty()) {
3739 error = tr("Running acidcam-gpu before Deep Dream cannot be combined "
3740 "with an ONNX input effect.");
3741 return false;
3742 }
3743 return true;
3744}
3745
3746void MainWindow::appendDeepDreamArguments(QStringList &arguments) const {
3748 return;
3749 }
3750
3751 arguments << "--dream-model" << deep_dream_model;
3752 arguments << "--dream-layer" << deep_dream_layer;
3753 arguments << "--dream-iterations" << QString::number(deep_dream_iterations);
3754 arguments << "--dream-strength" << QString::number(deep_dream_strength, 'g', 12);
3755 arguments << "--dream-feedback" << QString::number(deep_dream_feedback, 'g', 12);
3756 arguments << "--dream-zoom" << QString::number(deep_dream_zoom, 'g', 12);
3757 arguments << "--dream-rotation" << QString::number(deep_dream_rotation, 'g', 12);
3758 arguments << "--dream-size" << QString::number(deep_dream_maximum_dimension);
3759 if (deep_dream_fp16) {
3760 arguments << "--dream-fp16";
3761 }
3762 arguments << "--dream-channel" << (deep_dream_channel < 0 ? QString("all") : QString::number(deep_dream_channel));
3763 arguments << "--dream-octaves" << QString::number(deep_dream_octaves);
3764 arguments << "--dream-octave-scale" << QString::number(deep_dream_octave_scale, 'g', 12);
3765 arguments << "--dream-jitter" << QString::number(deep_dream_jitter);
3766 arguments << "--dream-smoothing" << QString::number(deep_dream_smoothing);
3768 arguments << "--gpu-filter-before-dream";
3769 }
3770 if (deep_dream_original) {
3771 arguments << "--deep-orig";
3772 }
3773}
3774
3777 QMessageBox::information(this,
3778 tr("Stable Diffusion Settings"),
3779 tr("Stable Diffusion is unavailable: ACMXVK must be built with "
3780 "-DWITH_STABLE_DIFFUSION=ON."));
3781 return;
3782 }
3783
3787 stableDiffusionSettingsDialog->activateWindow();
3788 return;
3789 }
3790
3792 stableDiffusionSettingsDialog->setAttribute(Qt::WA_DeleteOnClose);
3794 connect(dialog, &StableDiffusionSettingsDialog::settingsApplied, this, [this, dialog]() {
3795 const StableDiffusionConfiguration config = dialog->configuration();
3813 stable_diffusion_seed = config.seed;
3818
3820 Log(tr("Stable Diffusion Settings Applied: %1, %2x%3, "
3821 "%4 step(s)%5%6; changes apply on the next launch")
3822 .arg(stable_diffusion_upscale_only ? tr("ESRGAN upscale only") : QFileInfo(stable_diffusion_model).fileName())
3826 .arg(stable_diffusion_upscale_only ? tr(", before shader chain")
3827 : stable_diffusion_upscale ? tr(", compute upscale")
3828 : !stable_diffusion_upscale_model.isEmpty() ? tr(", ESRGAN upscale")
3829 : QString())
3830 .arg(stable_diffusion_upscale_only || stable_diffusion_lora_files.isEmpty() ? QString() : tr(", %1 LoRA(s)").arg(stable_diffusion_lora_files.size())));
3831 } else {
3832 Log("Stable Diffusion Disabled");
3833 }
3834 });
3835 dialog->show();
3836 dialog->raise();
3837 dialog->activateWindow();
3838}
3839
3841 error.clear();
3843 return true;
3844 }
3846 error = tr("The selected ACMXVK executable does not provide Stable "
3847 "Diffusion support.");
3848 return false;
3849 }
3850 if (video_file.trimmed().isEmpty() || !QFileInfo(video_file).isFile()) {
3851 error = tr("Stable Diffusion requires an existing video input file.");
3852 return false;
3853 }
3854 if (!graphics_file.trimmed().isEmpty()) {
3855 error = tr("Stable Diffusion cannot be used with still-image input.");
3856 return false;
3857 }
3858 if (!stable_diffusion_upscale_only && !QFileInfo(stable_diffusion_model).isFile()) {
3859 error = tr("The configured Stable Diffusion model does not exist:\n%1").arg(stable_diffusion_model);
3860 return false;
3861 }
3863 error = tr("The saved Stable Diffusion LoRA settings are incomplete. "
3864 "Open Stable Diffusion Settings and apply them again.");
3865 return false;
3866 }
3867 QString lora_directory;
3868 for (int index = 0; !stable_diffusion_upscale_only && index < stable_diffusion_lora_files.size(); ++index) {
3869 const QString filename = stable_diffusion_lora_files.at(index);
3870 const QFileInfo file_info(filename);
3871 if (!file_info.isFile()) {
3872 error = tr("The configured LoRA model does not exist:\n%1").arg(filename);
3873 return false;
3874 }
3875 if (lora_directory.isEmpty()) {
3876 lora_directory = file_info.absolutePath();
3877 } else if (lora_directory != file_info.absolutePath()) {
3878 error = tr("All configured LoRA models must be in the same folder. "
3879 "sd-server scans one LoRA model directory per launch.");
3880 return false;
3881 }
3882 const double multiplier = stable_diffusion_lora_multipliers.at(index);
3883 if (multiplier < -10.0 || multiplier > 10.0) {
3884 error = tr("LoRA multipliers must be between -10 and 10.");
3885 return false;
3886 }
3887 }
3888 if (!stable_diffusion_upscale_model.isEmpty() && !QFileInfo(stable_diffusion_upscale_model).isFile()) {
3889 error = tr("The configured Stable Diffusion upscale model does not "
3890 "exist:\n%1")
3892 return false;
3893 }
3895 error = tr("ESRGAN upscale-only mode requires an existing upscale model.");
3896 return false;
3897 }
3898 if (!stable_diffusion_upscale_only && stable_diffusion_prompt.trimmed().isEmpty()) {
3899 error = tr("Enter a Stable Diffusion image-to-image prompt.");
3900 return false;
3901 }
3902 if (stable_diffusion_server.trimmed().isEmpty()) {
3903 error = tr("Enter the sd-server executable name or path.");
3904 return false;
3905 }
3907 error = tr("Stable Diffusion dimensions must be multiples of 64.");
3908 return false;
3909 }
3911 error = tr("Stable Diffusion cannot be combined with Fill PTS Gaps.");
3912 return false;
3913 }
3914 return true;
3915}
3916
3917void MainWindow::appendStableDiffusionArguments(QStringList &arguments) const {
3919 return;
3920 }
3922 arguments << "--sd-upscale-only";
3923 } else {
3924 arguments << "--sd-model" << stable_diffusion_model;
3925 arguments << "--sd-prompt" << stable_diffusion_prompt;
3926 if (!stable_diffusion_negative_prompt.trimmed().isEmpty()) {
3927 arguments << "--sd-negative-prompt" << stable_diffusion_negative_prompt;
3928 }
3929 for (int index = 0; index < stable_diffusion_lora_files.size(); ++index) {
3930 arguments << "--sd-lora" << stable_diffusion_lora_files.at(index);
3931 arguments << "--sd-lora-strength" << QString::number(stable_diffusion_lora_multipliers.at(index), 'g', 12);
3932 }
3933 }
3934 arguments << "--sd-server" << stable_diffusion_server;
3935 const QStringList server_arguments = QProcess::splitCommand(stable_diffusion_server_arguments);
3936 for (const QString &argument : server_arguments) {
3937 arguments << "--sd-server-arg" << argument;
3938 }
3939 arguments << "--sd-server-port" << QString::number(stable_diffusion_server_port);
3941 arguments << "--sd-upscale-size" << QString("%1x%2").arg(stable_diffusion_upscale_working_width).arg(stable_diffusion_upscale_working_height);
3942 }
3944 arguments << "--sd-size" << QString("%1x%2").arg(stable_diffusion_width).arg(stable_diffusion_height);
3945 arguments << "--sd-steps" << QString::number(stable_diffusion_steps);
3946 arguments << "--sd-strength" << QString::number(stable_diffusion_strength, 'g', 12);
3947 arguments << "--sd-cfg-scale" << QString::number(stable_diffusion_cfg_scale, 'g', 12);
3948 arguments << "--sd-seed" << QString::number(stable_diffusion_seed);
3949 arguments << "--sd-sampler" << stable_diffusion_sampler;
3950 arguments << "--sd-scheduler" << stable_diffusion_scheduler;
3951 }
3952 arguments << "--sd-quiet";
3954 arguments << "--sd-upscale";
3955 } else if (!stable_diffusion_upscale_model.isEmpty()) {
3956 arguments << "--upscale-model" << stable_diffusion_upscale_model;
3957 }
3958}
3959
3961 if (!midi_available) {
3962 QMessageBox::information(this, tr("MIDI Settings"), tr("MIDI support is unavailable: acmx2 was built without MIDI support."));
3963 return;
3964 }
3965 MidiSettings midiDialog(executable_path, this);
3966 if (midiDialog.exec() == QDialog::Accepted) {
3967 midi_enabled = midiDialog.isMidiEnabled();
3968 midi_config_file = midiDialog.getMidiConfigFile();
3969 midi_device = midiDialog.getMidiDeviceIndex();
3970 QSettings appSettings("LostSideDead");
3971 appSettings.setValue("midiEnabled", midi_enabled);
3972 appSettings.setValue("midiConfigFile", midi_config_file);
3973 appSettings.setValue("midiDevice", midi_device);
3974 if (midi_enabled) {
3975 Log("MIDI Settings Saved: Config=" + midi_config_file + ", Device=" + QString::number(midi_device));
3976 } else {
3977 Log("MIDI Disabled");
3978 }
3979 }
3980}
3981
3983 display_filter_enabled = checked;
3984 QSettings appSettings("LostSideDead");
3985 appSettings.setValue("displayFilter", display_filter_enabled);
3986 Log(QString("Display Filter Overlay: %1").arg(display_filter_enabled ? "Enabled" : "Disabled"));
3988}
3989
3991 QDialog dlg(this);
3992 dlg.setWindowTitle(tr("Watermark Settings"));
3994
3995 auto *enableCheck = new QCheckBox(tr("Enable watermark in recorded video"), &dlg);
3996 enableCheck->setChecked(watermark_enabled);
3997
3998 auto *textEdit = new QLineEdit(watermark_text, &dlg);
3999 textEdit->setPlaceholderText(tr("Watermark text (shown upper-left of recorded video)"));
4000
4001 auto *colorPreview = new QLabel(&dlg);
4002 colorPreview->setAutoFillBackground(true);
4003 colorPreview->setMinimumSize(80, 24);
4004 colorPreview->setFrameStyle(QFrame::Box | QFrame::Plain);
4005 colorPreview->setAlignment(Qt::AlignCenter);
4006
4007 int curR = watermark_r, curG = watermark_g, curB = watermark_b;
4008 auto applyPreview = [colorPreview, &curR, &curG, &curB]() {
4009 QPalette pal = colorPreview->palette();
4010 pal.setColor(QPalette::Window, QColor(curR, curG, curB));
4011 QColor fg = (curR * 0.299 + curG * 0.587 + curB * 0.114) > 140 ? Qt::black : Qt::white;
4012 pal.setColor(QPalette::WindowText, fg);
4013 colorPreview->setPalette(pal);
4014 colorPreview->setText(QString(" %1, %2, %3 ").arg(curR).arg(curG).arg(curB));
4015 };
4016 applyPreview();
4017
4018 auto *colorBtn = new QPushButton(tr("Choose Color..."), &dlg);
4019 QObject::connect(colorBtn, &QPushButton::clicked, &dlg, [&]() {
4020 QColor chosen = QColorDialog::getColor(QColor(curR, curG, curB), &dlg, tr("Watermark Color"));
4021 if (chosen.isValid()) {
4022 curR = chosen.red();
4023 curG = chosen.green();
4024 curB = chosen.blue();
4025 applyPreview();
4026 }
4027 });
4028
4029 auto *form = new QFormLayout();
4030 form->addRow(enableCheck);
4031 form->addRow(tr("Text:"), textEdit);
4032 auto *colorRow = new QHBoxLayout();
4033 colorRow->addWidget(colorPreview, 1);
4034 colorRow->addWidget(colorBtn);
4035 form->addRow(tr("Color:"), colorRow);
4036
4037 auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
4038 QObject::connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
4039 QObject::connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
4040
4041 auto *layout = new QVBoxLayout(&dlg);
4042 layout->addLayout(form);
4043 layout->addWidget(buttons);
4044
4045 if (dlg.exec() != QDialog::Accepted) {
4046 return;
4047 }
4048
4049 watermark_enabled = enableCheck->isChecked();
4050 watermark_text = textEdit->text();
4051 watermark_r = curR;
4052 watermark_g = curG;
4053 watermark_b = curB;
4054
4055 QSettings appSettings("LostSideDead");
4056 appSettings.setValue("watermarkEnabled", watermark_enabled);
4057 appSettings.setValue("watermarkText", watermark_text);
4058 appSettings.setValue("watermarkR", watermark_r);
4059 appSettings.setValue("watermarkG", watermark_g);
4060 appSettings.setValue("watermarkB", watermark_b);
4061
4062 Log(QString("Watermark %1: \"%2\" color=%3,%4,%5").arg(watermark_enabled ? "Enabled" : "Disabled").arg(watermark_text).arg(watermark_r).arg(watermark_g).arg(watermark_b));
4064}
4065
4067 if (shader_path.isEmpty()) {
4068 QMessageBox::information(this, "Load Shaders First", "Please load a shader library before configuring multi-pass shaders.");
4069 return;
4070 }
4071
4072 loadShaders(shader_path, true);
4073
4074 if (items.isEmpty()) {
4075 QMessageBox::information(this, "Load Shaders First", "Please load a shader library before configuring multi-pass shaders.");
4076 return;
4077 }
4078
4079 if (shaderPassDialog) {
4080 shaderPassDialog->updateShaderList(items);
4081 shaderPassDialog->show();
4082 shaderPassDialog->raise();
4083 shaderPassDialog->activateWindow();
4084 return;
4085 }
4086
4088 shaderPassDialog->setAttribute(Qt::WA_DeleteOnClose);
4090 if (!shader_pass_names.isEmpty()) {
4091 shaderPassDialog->setSelectedShaderNames(shader_pass_names);
4092 }
4093
4095 auto applyMultipassSettings = [this, dialog]() {
4099 if (shader_pass_enabled) {
4100 Log("Multi-Pass Shader Settings Saved: " + QString::number(shader_pass_names.size()) + " passes");
4101 } else {
4102 Log("Multi-Pass Shader Disabled");
4103 }
4104 };
4105
4106 connect(dialog, &ShaderPassDialog::settingsApplied, this, [this](bool enabled, const QStringList &selectedShaderNames) {
4107 shader_pass_enabled = enabled;
4108 shader_pass_names = selectedShaderNames;
4110 if (shader_pass_enabled) {
4111 Log("Multi-Pass Shader Settings Saved: " + QString::number(shader_pass_names.size()) + " passes");
4112 } else {
4113 Log("Multi-Pass Shader Disabled");
4114 }
4115 });
4116 connect(dialog, &ShaderPassDialog::shaderEditRequested, this, [this](const QString &shaderName) {
4117 const QString safeName = sanitizeShaderName(shaderName);
4118 if (!safeName.isEmpty())
4119 openShaderEditor(QDir(shader_path).filePath(safeName));
4120 });
4121 connect(dialog, &QDialog::accepted, this, applyMultipassSettings);
4122
4123 dialog->show();
4124 dialog->raise();
4125 dialog->activateWindow();
4126}
4127
4129 if (shader_path.isEmpty()) {
4130 QMessageBox::information(this, "Load Shaders First", "Please load a shader library before configuring playlist.");
4131 return;
4132 }
4133
4134 loadShaders(shader_path, true);
4135
4136 if (items.isEmpty()) {
4137 QMessageBox::information(this, "Load Shaders First", "Please load a shader library before configuring playlist.");
4138 return;
4139 }
4140
4141 if (playlistDialog) {
4142 playlistDialog->updateShaderList(items);
4143 playlistDialog->show();
4144 playlistDialog->raise();
4145 playlistDialog->activateWindow();
4146 return;
4147 }
4148
4150 playlistDialog->setAttribute(Qt::WA_DeleteOnClose);
4151 playlistDialog->setEnabled(playlist_enabled);
4152 if (!playlist_tree_data.isEmpty()) {
4153 playlistDialog->setPlaylistTree(playlist_tree_data);
4154 } else if (!playlist_names.isEmpty()) {
4155 playlistDialog->setSelectedShaderNames(playlist_names);
4156 }
4157 if (!playlist_file_path.isEmpty()) {
4158 playlistDialog->setPlaylistFile(playlist_file_path);
4159 }
4160 playlistDialog->setAutopilotFrames(autopilot_frames);
4161 playlistDialog->setAutopilotRandom(autopilot_random);
4162
4164 connect(dialog, &QDialog::accepted, this, [this, dialog]() {
4171 QSettings appSettings("LostSideDead");
4172 appSettings.setValue("playlistAutopilotFrames", autopilot_frames);
4173 appSettings.setValue("playlistAutopilotRandom", autopilot_random);
4174 if (playlist_enabled) {
4175 Log("Playlist Settings Saved: " + QString::number(playlist_names.size()) + " shaders");
4176 if (!playlist_file_path.isEmpty()) {
4177 Log("Playlist file: " + playlist_file_path);
4178 }
4179 if (autopilot_frames > 0) {
4180 Log(QString("Autopilot timeout mode: %1 (%2 frames)").arg(autopilot_random ? "random" : "fixed").arg(autopilot_frames));
4181 }
4182 } else {
4183 Log("Playlist Disabled");
4184 }
4185 });
4186
4187 dialog->show();
4188 dialog->raise();
4189 dialog->activateWindow();
4190}
4191
4193 SettingsWindow settingsWindow(executable_path, active_backend, this);
4195 settingsWindow.setDnnAvailable(dnn_available);
4196 if (settingsWindow.exec() == QDialog::Accepted) {
4197 full_screen_value = settingsWindow.isFullscreen();
4198 if (settingsWindow.isUsingInputVideoFile()) {
4199 QString videoFile = settingsWindow.getInputVideoFile();
4200 QSize screenResolution = settingsWindow.getSelectedScreenResolution();
4201 screen_res = screenResolution;
4202 video_file = videoFile;
4203 graphics_file = "";
4204 cache_enabled = settingsWindow.isTextureCacheEnabled();
4205 cache_delay = settingsWindow.getCacheDelay();
4206 cache_size = settingsWindow.getCacheSize();
4207 copy_audio = settingsWindow.isCopyAudioEnabled();
4208 } else if (settingsWindow.isUsingGraphicsFile()) {
4209 QString graphicsFile = settingsWindow.getGraphicsFile();
4210 QSize screenResolution = settingsWindow.getSelectedScreenResolution();
4211 screen_res = screenResolution;
4212 graphics_file = graphicsFile;
4213 video_file = "";
4214 output_fps = settingsWindow.getCameraFPS();
4215 cache_enabled = false;
4216 cache_delay = 1;
4217 cache_size = 8;
4218 copy_audio = false;
4219 } else {
4220 int cameraIndex = settingsWindow.getSelectedCameraIndex();
4221 QSize cameraResolution = settingsWindow.getSelectedCameraResolution();
4222 QSize screenResolution = settingsWindow.getSelectedScreenResolution();
4223 screen_res = screenResolution;
4224 camera_index = cameraIndex;
4225 video_file = "";
4226 graphics_file = "";
4227 camera_res = cameraResolution;
4228 output_fps = settingsWindow.getCameraFPS();
4229 cache_enabled = settingsWindow.isTextureCacheEnabled();
4230 cache_delay = settingsWindow.getCacheDelay();
4231 cache_size = settingsWindow.getCacheSize();
4232 use_yuv = settingsWindow.isUseYuvEnabled();
4233 }
4234 if (settingsWindow.isSavingToOutputVideoFile()) {
4235 output_file = settingsWindow.getOutputVideoFile();
4236 save_output_log = settingsWindow.isSavingOutputLog();
4237 } else {
4238 output_file = "";
4239 save_output_log = false;
4240 }
4241 // Only meaningful in input-video mode + with an output file. The
4242 // settings dialog already gates this on HDR detection, but we re-check
4243 // here so it stays consistent if other modes are selected.
4244 convert_to_hdr10 = settingsWindow.isConvertToHdr10Enabled() && settingsWindow.isUsingInputVideoFile() && settingsWindow.isSavingToOutputVideoFile();
4245 maximize_fps = settingsWindow.isMaximizeFpsEnabled();
4246 use_source_fps = settingsWindow.isUseSourceFpsEnabled();
4247 use_source_audio = settingsWindow.isUseSourceAudioEnabled();
4248 }
4249 enable_3d = settingsWindow.is3dEnabled();
4250 model_file = settingsWindow.getModelFile();
4251 onnx_model_enabled = settingsWindow.isOnnxModelEnabled();
4252 onnx_model = settingsWindow.getOnnxModelFile();
4253 cuda_device = settingsWindow.getSelectedCudaDevice();
4254 time_speed = settingsWindow.getTimeSpeed();
4256 max_duration = settingsWindow.getDurationLimit();
4258 max_size_mb = settingsWindow.getMaxSizeLimit();
4259 cross_fade_duration = settingsWindow.getCrossFadeDuration();
4260 flip_enabled = settingsWindow.isFlipEnabled();
4261 rotate_enabled = settingsWindow.is_rotate_enabled();
4262 rotation_mode = settingsWindow.get_rotation_mode();
4265 png_level = settingsWindow.getPngLevel();
4266 generate_enabled = settingsWindow.isGenerateEnabled();
4267 generate_interval = settingsWindow.getGenerateInterval();
4268 encode_preset = settingsWindow.getEncodePreset();
4269 encode_tune = settingsWindow.getEncodeTune();
4270 encode_crf = settingsWindow.getEncodeCrf();
4271 encode_rate_control = settingsWindow.getEncodeRateControl();
4272 encode_bitrate = settingsWindow.getEncodeBitrate();
4273 encode_codec = settingsWindow.getEncodeCodec();
4274 encode_parameters = settingsWindow.getEncodeParameters();
4275 encode_realtime = settingsWindow.isEncodeRealtime();
4276 encode_no_drop = settingsWindow.isEncodeNoDrop();
4278 encode_fill_pts_gaps = settingsWindow.isEncodeFillPtsGaps();
4279}
4280
4282 if (process->state() == QProcess::Running) {
4283 QMessageBox::information(this, "Process Running", "A process is already running. Please stop it first.");
4284 return;
4285 }
4286
4287 QString deep_dream_error;
4288 if (!validateDeepDreamLaunch(deep_dream_error)) {
4289 QMessageBox::warning(this, tr("Deep Dream Settings"), deep_dream_error);
4290 return;
4291 }
4292 QString stable_diffusion_error;
4293 if (!validateStableDiffusionLaunch(stable_diffusion_error)) {
4294 QMessageBox::warning(this, tr("Stable Diffusion Settings"), stable_diffusion_error);
4295 return;
4296 }
4297
4298#ifdef __linux__
4299 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
4300 for (const QString &entry : defaultLinuxRunEnvAssignments()) {
4301 const int equals = entry.indexOf('=');
4302 if (equals > 0) {
4303 env.insert(entry.left(equals), entry.mid(equals + 1));
4304 }
4305 }
4306 process->setProcessEnvironment(env);
4307#endif
4308
4309 if (shader_path.length() == 0) {
4310 QMessageBox::information(this, "Select Shaders", "Select Shader Path");
4311 return;
4312 }
4317 const QString data = currentShaderName();
4318 if (data.isEmpty()) {
4319 Log("<b>No item selected.</b>");
4320 return;
4321 }
4322 QString launchShaderPath = shader_path;
4323 QString launchShaderName = data;
4325 QString runtimeError;
4326 if (!resolve_acmxvk_runtime_library(shader_path, launchShaderPath, runtimeError)) {
4328 return;
4329 }
4330 if (launchShaderPath != shader_path)
4331 launchShaderName = acmxvk_runtime_shader_name(data);
4332 if (!QFileInfo(QDir(launchShaderPath).filePath(launchShaderName)).isFile()) {
4333 QMessageBox::warning(this,
4334 tr("Build ACMXVK Library"),
4335 tr("The compiled shader is missing. Choose Playback > Build "
4336 "and try again.\n\n%1")
4337 .arg(QDir(launchShaderPath).filePath(launchShaderName)));
4338 return;
4339 }
4340 }
4341 QStringList arguments;
4342 QString dirPath = QCoreApplication::applicationDirPath();
4343#ifdef BUILD_BUNDLE
4345#endif
4346 dirPath = resolve_backend_assets_path(active_backend, executable_path, shader_path);
4347 const int selectedIndex = currentShaderRow();
4348 if (selectedIndex < 0 || selectedIndex >= items.size()) {
4349 Log("<b>No valid shader selection.</b>");
4350 return;
4351 }
4353 arguments << "--unbuffered";
4354 arguments << "--path" << dirPath;
4356 // ACMXVK needs its manifest to resolve fragment/compute types and
4357 // custom-uniform metadata for the selected SPIR-V shader.
4358 arguments << "--shaders" << launchShaderPath << "--shader-file" << launchShaderName << "--interface-shm";
4359 } else {
4360 // ACMX2 can compile a selected source directly without loading the
4361 // complete shader library and its binary cache.
4362 arguments << "--fragment" << (shader_path + "/" + data) << "--interface-shm";
4363 }
4364 // Pass texture cache size so the SIZE macro injected into the fragment
4365 // matches whatever the user has configured for cache shaders.
4366 arguments << "--texture-cache-size" << QString::number(cache_size > 0 ? cache_size : 8);
4367 if (cache_enabled && textureCacheArraySettingEnabled())
4368 arguments << "--texture-cache-array";
4369 const QSize effectiveCameraResolution = hasPositiveResolution(camera_res) ? camera_res : QSize(1280, 720);
4370 QString res;
4371 QTextStream stream(&res);
4372 stream << effectiveCameraResolution.width() << "x" << effectiveCameraResolution.height();
4373
4374 QString scr_res;
4375 QTextStream stream_r(&scr_res);
4376 stream_r << screen_res.width() << "x" << screen_res.height();
4377
4379 arguments << "--fullscreen";
4380
4381 if (!graphics_file.isEmpty()) {
4382 arguments << "--graphic" << graphics_file;
4383 if (hasPositiveResolution(screen_res))
4384 arguments << "--resolution" << scr_res;
4385 arguments << "--fps" << QString::number(output_fps);
4386 } else if (video_file.isEmpty()) {
4387 arguments << "--camera-res" << res;
4388 if (hasPositiveResolution(screen_res))
4389 arguments << "--resolution" << scr_res;
4390 arguments << "--device" << QString::number(camera_index);
4391 arguments << "--fps" << QString::number(output_fps);
4393 arguments << "--maximize-fps";
4394 if (use_yuv)
4395 arguments << "--use-yuv";
4396 if (cache_enabled) {
4397 arguments << "--texture-cache";
4398 arguments << "--cache-delay" << QString::number(cache_delay);
4399 }
4400 } else {
4401 arguments << "--input" << video_file;
4403 arguments << "--use-source-fps";
4404 if (use_source_audio)
4405 arguments << "--use-source-audio";
4406 }
4407 if (hasPositiveResolution(screen_res))
4408 arguments << "--resolution" << scr_res;
4409 if (play_repeat->isChecked())
4410 arguments << "--repeat";
4411 if (cache_enabled) {
4412 arguments << "--texture-cache";
4413 arguments << "--cache-delay" << QString::number(cache_delay);
4414 }
4415 if (copy_audio)
4416 arguments << "--copy-audio";
4417 }
4418 arguments << "--prefix" << prefix_path;
4420 arguments << "--png-level" << QString::number(png_level);
4421 }
4422
4423 if (!output_file.isEmpty()) {
4424 arguments << "--output" << output_file;
4426 arguments << "--video-bitrate" << encode_bitrate;
4427 else
4428 arguments << "--encode-crf" << QString::number(encode_crf);
4429 if (!encode_preset.isEmpty())
4430 arguments << "--encode-preset" << encode_preset;
4431 if (!encode_tune.isEmpty())
4432 arguments << "--encode-tune" << encode_tune;
4433 if (!encode_codec.isEmpty() && encode_codec != "auto")
4434 arguments << "--encode-codec" << encode_codec;
4435 if (!encode_parameters.isEmpty())
4436 arguments << "--encode-params" << encode_parameters;
4437 if (encode_realtime)
4438 arguments << "--encode-realtime";
4439 if (encode_no_drop && (!video_file.isEmpty() || !graphics_file.isEmpty()))
4440 arguments << "--no-drop";
4442 arguments << "--constant-frame-rate";
4444 arguments << "--fill-pts-gaps";
4445 }
4446 const bool sourceAudioActive = active_backend == acmx2::Backend::Acmxvk && !video_file.isEmpty() && use_source_fps && use_source_audio;
4447 if (audio_available && audio_enabled && !sourceAudioActive) {
4448 arguments << "--enable-audio";
4449 arguments << "--channels" << QString::number(audio_channels);
4450
4451 if (audio_input == -1)
4452 arguments << "--audio-input" << "default";
4453 else
4454 arguments << "--audio-input" << QString::number(audio_input);
4455
4456 if (record_audio) {
4457 QString wavPath;
4458 if (!output_file.isEmpty()) {
4459 QFileInfo fi(output_file);
4460 wavPath = fi.absolutePath() + "/" + fi.completeBaseName() + ".wav";
4461 } else {
4462 wavPath = prefix_path + "/recorded_audio.wav";
4463 }
4464 arguments << "--record-audio" << wavPath;
4465 arguments << "--record-gain" << QString::number(record_volume, 'f', 2);
4466 }
4467 }
4468
4469 if (active_backend == acmx2::Backend::Acmxvk && !record_audio && (audio_enabled || !audio_file.isEmpty() || sourceAudioActive)) {
4470 arguments << "--mute-output";
4471 }
4472
4473 if (audio_available && (audio_enabled || !audio_file.isEmpty() || sourceAudioActive)) {
4474 arguments << "--sense" << QString::number(audio_sense);
4475 if (audio_passthrough) {
4476 arguments << "--pass-through";
4477 if (audio_output == -1)
4478 arguments << "--audio-output" << "default";
4479 else
4480 arguments << "--audio-output" << QString::number(audio_output);
4481 }
4482 }
4483
4484 if (audio_available && !audio_file.isEmpty() && !sourceAudioActive) {
4485 arguments << "--audio-file" << audio_file;
4486 if (audio_trunc) {
4487 arguments << "--audio-trunc";
4488 }
4489 if (audio_repeat) {
4490 arguments << "--audio-repeat";
4491 }
4492 }
4493
4495 arguments << "--enable-audio-buffers" << QString::number(audio_buffer_frames);
4496 }
4497
4498 if (audio_available && (audio_enabled || !audio_file.isEmpty() || sourceAudioActive)) {
4499 arguments << "--audio-warm-rate" << QString::number(audio_warm_rate, 'f', 2);
4500 }
4501
4502 if (enable_3d) {
4503 arguments << "--enable-3d";
4504 arguments << "--model" << model_file;
4505 }
4506
4507 if (onnx_model_enabled && !onnx_model.isEmpty()) {
4508 arguments << "--onnx" << onnx_model;
4509 }
4510
4512 arguments << "--gpu-filter" << gpu_filter_indices;
4513 arguments << "--gpu-buffer" << QString::number(gpu_buffer_size);
4514 }
4515
4516 appendDeepDreamArguments(arguments);
4518
4520 arguments << "--cuda-device" << QString::number(cuda_device);
4521 }
4522
4523 arguments << "--time-speed" << QString::number(static_cast<double>(time_speed), 'f', 2);
4524 if (normalized_time) {
4525 arguments << "--normalized";
4526 }
4527
4529 arguments << "--no-cache";
4530 }
4531
4532 if (midi_available && midi_enabled && !midi_config_file.isEmpty()) {
4533 arguments << "--midi-map" << midi_config_file;
4534 if (midi_device >= 0)
4535 arguments << "--midi-device" << QString::number(midi_device);
4536 }
4537
4539 arguments << "--duration" << QString::number(max_duration, 'f', 1);
4540 }
4541
4542 if (!output_file.isEmpty() && max_size_limit_enabled && max_size_mb > 0.0) {
4543 arguments << "--max-size" << QString::number(max_size_mb, 'f', 2);
4544 }
4545
4546 if (cross_fade_duration != 0.5f) {
4547 arguments << "--cross-fade" << QString::number(static_cast<double>(cross_fade_duration), 'f', 2);
4548 }
4549
4550 if (flip_enabled) {
4551 arguments << "--flip";
4552 }
4553
4554 if (rotate_enabled) {
4555 arguments << "--rotate" << rotation_mode;
4556 }
4557
4559 arguments << "--png" << png_output_directory;
4560 }
4561
4562 const int effective_generate_interval = generate_enabled ? generate_interval : (active_backend == acmx2::Backend::Acmxvk && png_output ? 1 : 0);
4563 if (effective_generate_interval > 0) {
4564 arguments << "--generate" << QString::number(effective_generate_interval);
4565 }
4566
4567 if (watermark_enabled && !watermark_text.isEmpty()) {
4568 arguments << "--use-watermark" << watermark_text;
4569 arguments << "--use-watermark-color" << QString("%1,%2,%3").arg(watermark_r).arg(watermark_g).arg(watermark_b);
4570 }
4571
4573 arguments << "--display-filter";
4574 }
4575
4576 arguments.append(QProcess::splitCommand(extra_arguments));
4577
4578 // ACMX2 single-source mode bypasses its binary cache. ACMXVK uses the
4579 // selected runtime library and does not accept ACMX2 cache controls.
4580 Log("shell: " + executable_path + " " + concatList(arguments) + "<br>");
4581 beginOutputRunLog(executable_path + " " + concatList(arguments));
4582 process->start(executable_path, arguments);
4583 if (!process->waitForStarted()) {
4584 appendOutputRunLog(tr("Failed to start: %1").arg(process->errorString()));
4585 finishOutputRunLog(-1, QProcess::CrashExit);
4586 Log("<b style='color:red;'>Failed to start the program.</b>");
4587 QMessageBox::critical(this, "Error", "Failed to start the program.");
4588 } else {
4589 play_stop->setEnabled(true);
4590 }
4591}
4592
4593bool MainWindow::buildRunArguments(QStringList &arguments, PendingAcmxvkAction resume_action, bool include_extra_arguments) {
4594 QString deep_dream_error;
4595 if (!validateDeepDreamLaunch(deep_dream_error)) {
4596 QMessageBox::warning(this, tr("Deep Dream Settings"), deep_dream_error);
4597 return false;
4598 }
4599 QString stable_diffusion_error;
4600 if (!validateStableDiffusionLaunch(stable_diffusion_error)) {
4601 QMessageBox::warning(this, tr("Stable Diffusion Settings"), stable_diffusion_error);
4602 return false;
4603 }
4604 if (shader_path.length() == 0) {
4605 QMessageBox::information(this, "Select Shaders", "Select Shader Path");
4606 return false;
4607 }
4608 int index = 0;
4609 const int row = currentShaderRow();
4610 if (row < 0) {
4611 index = 0;
4612 Log("No selection, defaulting to index 0");
4613 } else {
4614 index = row;
4615 const QString selectedData = currentShaderName();
4616 Log("Selected shader: " + selectedData + " at index: " + QString::number(index));
4617 }
4618 if (items.isEmpty()) {
4619 QMessageBox::warning(this, tr("Empty Shader Library"), tr("The selected shader library contains no shaders."));
4620 return false;
4621 }
4622 if (index < 0 || index >= items.size()) {
4623 QMessageBox::warning(this, tr("Invalid Shader Selection"), tr("Select a shader from the active library."));
4624 return false;
4625 }
4626 QString launchShaderPath = shader_path;
4627 QString launchShaderName = items.at(index);
4629 QString runtimeError;
4630 if (resume_action == PendingAcmxvkAction::CopyCommand) {
4631 if (is_acmxvk_source_library(shader_path, runtimeError)) {
4632 launchShaderPath = acmxvk_build_directory(shader_path);
4633 launchShaderName = acmxvk_runtime_shader_name(launchShaderName);
4634 } else if (!runtimeError.isEmpty()) {
4635 QMessageBox::warning(this, tr("ACMXVK Library"), runtimeError);
4636 return false;
4637 }
4638 } else {
4639 if (!resolve_acmxvk_runtime_library(shader_path, launchShaderPath, runtimeError)) {
4640 prompt_acmxvk_rebuild(runtimeError, resume_action);
4641 return false;
4642 }
4643 if (launchShaderPath != shader_path)
4644 launchShaderName = acmxvk_runtime_shader_name(launchShaderName);
4645 if (!QFileInfo(QDir(launchShaderPath).filePath(launchShaderName)).isFile()) {
4646 QMessageBox::warning(this,
4647 tr("Build ACMXVK Library"),
4648 tr("The compiled shader is missing. Choose Playback > "
4649 "Build and try again.\n\n%1")
4650 .arg(QDir(launchShaderPath).filePath(launchShaderName)));
4651 return false;
4652 }
4653 }
4654 }
4655 QString dirPath = QCoreApplication::applicationDirPath();
4656#ifdef BUILD_BUNDLE
4658#endif
4659 dirPath = resolve_backend_assets_path(active_backend, executable_path, shader_path);
4660
4661 QString shader_file = launchShaderPath;
4663 arguments << "--unbuffered";
4664 arguments << "--path" << dirPath << "--shaders" << shader_file;
4665 arguments << "--interface-shm";
4666 // Always pass texture cache size so runtime SIZE matches the cache file.
4667 arguments << "--texture-cache-size" << QString::number(cache_size > 0 ? cache_size : 8);
4668 if (cache_enabled && textureCacheArraySettingEnabled())
4669 arguments << "--texture-cache-array";
4670 const QSize effectiveCameraResolution = hasPositiveResolution(camera_res) ? camera_res : QSize(1280, 720);
4671 QString res;
4672 QTextStream stream(&res);
4673 stream << effectiveCameraResolution.width() << "x" << effectiveCameraResolution.height();
4674 QString scr_res;
4675 QTextStream stream_r(&scr_res);
4676 stream_r << screen_res.width() << "x" << screen_res.height();
4677
4679 arguments << "--fullscreen";
4680
4681 if (!graphics_file.isEmpty()) {
4682 arguments << "--graphic" << graphics_file;
4683 if (hasPositiveResolution(screen_res))
4684 arguments << "--resolution" << scr_res;
4685 arguments << "--fps" << QString::number(output_fps);
4686 } else if (video_file.isEmpty()) {
4687 arguments << "--camera-res" << res;
4688 if (hasPositiveResolution(screen_res))
4689 arguments << "--resolution" << scr_res;
4690 arguments << "--device" << QString::number(camera_index);
4691 arguments << "--fps" << QString::number(output_fps);
4693 arguments << "--maximize-fps";
4694 if (use_yuv)
4695 arguments << "--use-yuv";
4696 if (cache_enabled) {
4697 arguments << "--texture-cache";
4698 arguments << "--cache-delay" << QString::number(cache_delay);
4699 }
4700 } else {
4701 arguments << "--input" << video_file;
4703 arguments << "--use-source-fps";
4704 if (use_source_audio)
4705 arguments << "--use-source-audio";
4706 }
4707 if (hasPositiveResolution(screen_res))
4708 arguments << "--resolution" << scr_res;
4709 if (play_repeat->isChecked())
4710 arguments << "--repeat";
4711 if (cache_enabled) {
4712 arguments << "--texture-cache";
4713 arguments << "--cache-delay" << QString::number(cache_delay);
4714 }
4715 if (copy_audio)
4716 arguments << "--copy-audio";
4717 }
4718 arguments << "--prefix" << prefix_path;
4720 arguments << "--png-level" << QString::number(png_level);
4721 }
4722 if (!output_file.isEmpty()) {
4723 arguments << "--output" << output_file;
4725 arguments << "--video-bitrate" << encode_bitrate;
4726 else
4727 arguments << "--encode-crf" << QString::number(encode_crf);
4728 if (!encode_preset.isEmpty())
4729 arguments << "--encode-preset" << encode_preset;
4730 if (!encode_tune.isEmpty())
4731 arguments << "--encode-tune" << encode_tune;
4732 if (!encode_codec.isEmpty() && encode_codec != "auto")
4733 arguments << "--encode-codec" << encode_codec;
4734 if (!encode_parameters.isEmpty())
4735 arguments << "--encode-params" << encode_parameters;
4736 if (encode_realtime)
4737 arguments << "--encode-realtime";
4738 if (encode_no_drop && (!video_file.isEmpty() || !graphics_file.isEmpty()))
4739 arguments << "--no-drop";
4741 arguments << "--constant-frame-rate";
4743 arguments << "--fill-pts-gaps";
4744 }
4745 arguments << "--shader-file" << launchShaderName;
4746
4747 const bool sourceAudioActive = active_backend == acmx2::Backend::Acmxvk && !video_file.isEmpty() && use_source_fps && use_source_audio;
4748 if (audio_available && audio_enabled && !sourceAudioActive) {
4749 arguments << "--enable-audio";
4750 arguments << "--channels" << QString::number(audio_channels);
4751
4752 if (audio_input == -1)
4753 arguments << "--audio-input" << "default";
4754 else
4755 arguments << "--audio-input" << QString::number(audio_input);
4756
4757 if (record_audio) {
4758 QString wavPath;
4759 if (!output_file.isEmpty()) {
4760 QFileInfo fi(output_file);
4761 wavPath = fi.absolutePath() + "/" + fi.completeBaseName() + ".wav";
4762 } else {
4763 wavPath = prefix_path + "/recorded_audio.wav";
4764 }
4765 arguments << "--record-audio" << wavPath;
4766 arguments << "--record-gain" << QString::number(record_volume, 'f', 2);
4767 }
4768 }
4769
4770 if (active_backend == acmx2::Backend::Acmxvk && !record_audio && (audio_enabled || !audio_file.isEmpty() || sourceAudioActive)) {
4771 arguments << "--mute-output";
4772 }
4773
4774 if (audio_available && (audio_enabled || !audio_file.isEmpty() || sourceAudioActive)) {
4775 arguments << "--sense" << QString::number(audio_sense);
4776 if (audio_passthrough) {
4777 arguments << "--pass-through";
4778 if (audio_output == -1)
4779 arguments << "--audio-output" << "default";
4780 else
4781 arguments << "--audio-output" << QString::number(audio_output);
4782 }
4783 }
4784
4785 if (audio_available && !audio_file.isEmpty() && !sourceAudioActive) {
4786 arguments << "--audio-file" << audio_file;
4787 if (audio_trunc) {
4788 arguments << "--audio-trunc";
4789 }
4790 if (audio_repeat) {
4791 arguments << "--audio-repeat";
4792 }
4793 }
4794
4796 arguments << "--enable-audio-buffers" << QString::number(audio_buffer_frames);
4797 }
4798
4799 if (audio_available && (audio_enabled || !audio_file.isEmpty() || sourceAudioActive)) {
4800 arguments << "--audio-warm-rate" << QString::number(audio_warm_rate, 'f', 2);
4801 }
4802
4803 if (enable_3d) {
4804 arguments << "--enable-3d";
4805 arguments << "--model" << model_file;
4806 }
4807
4808 if (onnx_model_enabled && !onnx_model.isEmpty()) {
4809 arguments << "--onnx" << onnx_model;
4810 }
4811
4813 arguments << "--gpu-filter" << gpu_filter_indices;
4814 arguments << "--gpu-buffer" << QString::number(gpu_buffer_size);
4815 }
4816
4817 appendDeepDreamArguments(arguments);
4819
4820 if (shader_pass_enabled && !shader_pass_names.isEmpty()) {
4821 QString passIndices = getShaderPassIndicesFromNames();
4822 if (!passIndices.isEmpty()) {
4823 QStringList passFiles;
4824 const QStringList indexValues = passIndices.split(',');
4825 for (const QString &indexValue : indexValues) {
4826 bool ok = false;
4827 const int passIndex = indexValue.toInt(&ok);
4828 if (ok && passIndex >= 0 && passIndex < items.size()) {
4829 const QString passFile = items.at(passIndex);
4830 passFiles.append(launchShaderPath == shader_path ? passFile : acmxvk_runtime_shader_name(passFile));
4831 }
4832 }
4833 QByteArray passFilePayload;
4834 for (const QString &passFile : passFiles) {
4835 const QByteArray encodedName = passFile.toUtf8();
4836 passFilePayload.append(QByteArray::number(encodedName.size()));
4837 passFilePayload.append(':');
4838 passFilePayload.append(encodedName);
4839 }
4840 arguments << "--shader-pass-files" << QString::fromUtf8(passFilePayload);
4841 }
4842 }
4843
4845 arguments << "--cuda-device" << QString::number(cuda_device);
4846 }
4847
4848 arguments << "--time-speed" << QString::number(static_cast<double>(time_speed), 'f', 2);
4849 if (normalized_time) {
4850 arguments << "--normalized";
4851 }
4852
4854 arguments << "--no-cache";
4855 }
4856
4857 if (midi_available && midi_enabled && !midi_config_file.isEmpty()) {
4858 arguments << "--midi-map" << midi_config_file;
4859 if (midi_device >= 0)
4860 arguments << "--midi-device" << QString::number(midi_device);
4861 }
4862
4863 const bool playlistActive = playlist_enabled && !playlist_names.isEmpty();
4864 if (playlistActive) {
4865 QString plFile = playlist_file_path;
4866 if (plFile.isEmpty()) {
4867 plFile = prefix_path + "/playlist.txt";
4868 }
4869 QFile f(plFile);
4870 if (f.open(QIODevice::WriteOnly | QIODevice::Text)) {
4871 QTextStream out(&f);
4872 if (!playlist_tree_data.isEmpty()) {
4873 for (const auto &[nodeName, shaders] : playlist_tree_data) {
4874 out << "[" << nodeName << "]\n";
4875 for (const QString &name : shaders) {
4876 out << (launchShaderPath == shader_path ? name : acmxvk_runtime_shader_name(name)) << "\n";
4877 }
4878 }
4879 } else {
4880 for (const QString &name : playlist_names) {
4881 out << (launchShaderPath == shader_path ? name : acmxvk_runtime_shader_name(name)) << "\n";
4882 }
4883 }
4884 f.close();
4885 playlist_file_path = plFile;
4886 }
4887 arguments << "--playlist" << plFile;
4888 }
4889
4890 if (playlistActive && autopilot_frames > 0) {
4891 arguments << (autopilot_random ? "--autopilot-random" : "--autopilot-frames") << QString::number(autopilot_frames);
4892 }
4893
4895 arguments << "--duration" << QString::number(max_duration, 'f', 1);
4896 }
4897
4898 if (!output_file.isEmpty() && max_size_limit_enabled && max_size_mb > 0.0) {
4899 arguments << "--max-size" << QString::number(max_size_mb, 'f', 2);
4900 }
4901
4902 if (cross_fade_duration != 0.5f) {
4903 arguments << "--cross-fade" << QString::number(static_cast<double>(cross_fade_duration), 'f', 2);
4904 }
4905
4906 if (flip_enabled) {
4907 arguments << "--flip";
4908 }
4909
4910 if (rotate_enabled) {
4911 arguments << "--rotate" << rotation_mode;
4912 }
4913
4915 arguments << "--png" << png_output_directory;
4916 }
4917
4918 const int effective_generate_interval = generate_enabled ? generate_interval : (active_backend == acmx2::Backend::Acmxvk && png_output ? 1 : 0);
4919 if (effective_generate_interval > 0) {
4920 arguments << "--generate" << QString::number(effective_generate_interval);
4921 }
4922
4923 if (watermark_enabled && !watermark_text.isEmpty()) {
4924 arguments << "--use-watermark" << watermark_text;
4925 arguments << "--use-watermark-color" << QString("%1,%2,%3").arg(watermark_r).arg(watermark_g).arg(watermark_b);
4926 }
4927
4929 arguments << "--display-filter";
4930 }
4931
4932 if (include_extra_arguments) {
4933 arguments.append(QProcess::splitCommand(extra_arguments));
4934 }
4935
4936 return true;
4937}
4938
4940 if (!hdr10Process) {
4941 return;
4942 }
4943 if (hdr10Process->state() == QProcess::Running) {
4944 Log("<b style='color:red;'>HDR10 conversion already running; skipping.</b>");
4945 return;
4946 }
4947 if (output_file.isEmpty() || !QFileInfo::exists(output_file)) {
4948 Log("<b style='color:red;'>HDR10 conversion: source file missing.</b>");
4949 return;
4950 }
4951
4952 QFileInfo fi(output_file);
4953 const QString suffix = fi.suffix();
4954 const QString hdr10Path = fi.absolutePath() + "/" + fi.completeBaseName() + ".HDR10" + (suffix.isEmpty() ? QString() : "." + suffix);
4955
4956 QStringList args;
4957 args << "-y"
4958 << "-i" << output_file;
4959
4960 // Honor the user's codec selection from the recording settings dialog.
4961 // Values come from the encodeCodecComboBox: "auto", "software", "nvenc".
4962 // "auto" picks NVENC if CUDA is available, otherwise libx265.
4963 const QString codecChoice = encode_codec.toLower();
4964 bool useNvenc;
4965 if (codecChoice == "software" || codecChoice == "libx265" || codecChoice == "x265") {
4966 useNvenc = false;
4967 } else if (codecChoice == "nvenc" || codecChoice == "hevc_nvenc") {
4968 useNvenc = true;
4969 } else {
4970 useNvenc = cuda_available; // "auto" or empty
4971 }
4972
4973 if (useNvenc) {
4974 // NVENC HEVC HDR10 path. p010le = 10-bit 4:2:0 semi-planar, required
4975 // by hevc_nvenc Main10. NVENC's preset namespace is p1..p7 (fastest
4976 // -> slowest); map the x264-style names from the UI combo onto it.
4977 QString nvencPreset;
4978 const QString p = encode_preset.toLower();
4979 if (p == "ultrafast")
4980 nvencPreset = "p1";
4981 else if (p == "superfast")
4982 nvencPreset = "p2";
4983 else if (p == "veryfast")
4984 nvencPreset = "p3";
4985 else if (p == "faster")
4986 nvencPreset = "p4";
4987 else if (p == "fast")
4988 nvencPreset = "p5";
4989 else if (p == "medium")
4990 nvencPreset = "p6";
4991 else if (p == "slow")
4992 nvencPreset = "p6";
4993 else if (p == "slower")
4994 nvencPreset = "p7";
4995 else if (p == "veryslow")
4996 nvencPreset = "p7";
4997 else if (p.startsWith("p") && p.size() == 2 && p[1].isDigit())
4998 nvencPreset = p; // already an NVENC preset
4999 else
5000 nvencPreset = "p6";
5001
5002 args << "-vf" << "zscale=p=bt2020:t=smpte2084:m=bt2020nc,format=p010le"
5003 << "-c:v" << "hevc_nvenc"
5004 << "-preset" << nvencPreset << "-tune" << "hq"
5005 << "-b:v" << "56M"
5006 << "-maxrate" << "60M"
5007 << "-bufsize" << "60M"
5008 << "-color_primaries" << "bt2020"
5009 << "-colorspace" << "bt2020nc"
5010 << "-color_trc" << "smpte2084";
5011 Log("HDR10 codec: hevc_nvenc (CUDA detected, preset=" + nvencPreset + ")<br>");
5012 } else {
5013 args << "-vf" << "zscale=p=bt2020:t=smpte2084:m=bt2020nc,format=yuv420p10le"
5014 << "-c:v" << "libx265"
5015 << "-preset" << (encode_preset.isEmpty() ? QStringLiteral("medium") : encode_preset) << "-b:v" << "56M"
5016 << "-maxrate" << "60M"
5017 << "-bufsize" << "60M"
5018 << "-pix_fmt" << "yuv420p10le"
5019 << "-x265-params"
5020 << "hdr10=1:hdr10-opt=1:repeat-headers=1:"
5021 "colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:range=limited:"
5022 "master-display=G(8500,39850)B(6550,2300)R(35400,14600)WP(15635,16450)L(10000000,1):"
5023 "max-cll=1000,400"
5024 << "-color_primaries" << "bt2020"
5025 << "-colorspace" << "bt2020nc"
5026 << "-color_trc" << "smpte2084";
5027 Log("HDR10 codec: libx265 (codec=" + (codecChoice.isEmpty() ? QStringLiteral("auto") : codecChoice) + ")<br>");
5028 }
5029
5030 args << "-c:a" << "copy" << hdr10Path;
5031
5032 Log("shell: ffmpeg " + concatList(args) + "<br>");
5033 Log("HDR10 output: " + hdr10Path + "<br>");
5034
5035 // ffmpeg writes most of its progress to stderr; merge channels so the
5036 // log keeps messages in source order.
5037 hdr10Process->setProcessChannelMode(QProcess::MergedChannels);
5038 hdr10Process->start("ffmpeg", args);
5039 if (!hdr10Process->waitForStarted(5000)) {
5040 Log("<b style='color:red;'>Failed to start ffmpeg for HDR10 conversion.</b>");
5041 return;
5042 }
5043 play_stop->setEnabled(true);
5044}
5045
5047 if (process->state() == QProcess::Running) {
5048 QMessageBox::information(this, "Process Running", "A process is already running. Please stop it first.");
5049 return;
5050 }
5051
5052#ifdef __linux__
5053 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
5054 for (const QString &entry : defaultLinuxRunEnvAssignments()) {
5055 int eq = entry.indexOf('=');
5056 if (eq <= 0) {
5057 continue;
5058 }
5059 env.insert(entry.left(eq), entry.mid(eq + 1));
5060 }
5061 process->setProcessEnvironment(env);
5062#endif
5063
5064 QStringList arguments;
5066 return;
5071
5072 Log("shell: " + executable_path + " " + concatList(arguments) + "<br>");
5073 beginOutputRunLog(executable_path + " " + concatList(arguments));
5074 process->start(executable_path, arguments);
5075 if (!process->waitForStarted()) {
5076 appendOutputRunLog(tr("Failed to start: %1").arg(process->errorString()));
5077 finishOutputRunLog(-1, QProcess::CrashExit);
5078 Log("<b style='color:red;'>Failed to start the program.</b>");
5079 QMessageBox::critical(this, "Error", "Failed to start the program.");
5080 } else {
5081 play_stop->setEnabled(true);
5082 }
5083}
5084
5086 QStringList arguments;
5088 return;
5089
5090 QString exe = executable_path;
5091 if (exe.isEmpty())
5093 QStringList envAssignments;
5094#ifdef __linux__
5095 envAssignments = defaultLinuxRunEnvAssignments();
5096#endif
5097 QString commandText = buildShellCommand(envAssignments, exe, arguments).trimmed();
5098
5099 QDialog dialog(this);
5100 dialog.setWindowTitle(tr("Edit Command"));
5101 dialog.resize(720, active_backend == acmx2::Backend::Acmxvk ? 360 : 320);
5103
5104 QVBoxLayout *layout = new QVBoxLayout(&dialog);
5105 QPlainTextEdit *textBox = new QPlainTextEdit(&dialog);
5106 textBox->setPlainText(commandText);
5107 textBox->setReadOnly(false);
5108 textBox->setLineWrapMode(QPlainTextEdit::WidgetWidth);
5110 textBox->setStyleSheet("QPlainTextEdit { background-color: black; color: lime; "
5111 "font-size: 14px; font-family: 'Courier New', Courier, monospace; "
5112 "border: 1px solid red; }");
5113 } else {
5114 QFont commandFont("Courier New");
5115 commandFont.setStyleHint(QFont::Monospace);
5116 commandFont.setPointSize(14);
5117 textBox->setFont(commandFont);
5118 }
5119 layout->addWidget(textBox);
5120
5121 auto *extraArgumentsEdit = new QLineEdit(&dialog);
5122 extraArgumentsEdit->setText(extra_arguments);
5123 extraArgumentsEdit->setPlaceholderText(tr("--option value --another-option \"value with spaces\""));
5124 extraArgumentsEdit->setToolTip(tr("These arguments are appended after the generated arguments for "
5125 "Run Selected, Run All, and this dialog's command. Use double "
5126 "quotes around values containing spaces."));
5127 auto *extraArgumentsLayout = new QFormLayout();
5128 extraArgumentsLayout->addRow(tr("Extra arguments:"), extraArgumentsEdit);
5129 layout->addLayout(extraArgumentsLayout);
5130
5132 QSettings settings("LostSideDead");
5133 const QString enabledKey = acmx2::backend_settings_key(acmx2::Backend::Acmxvk, "parallel_build_enabled");
5134 const QString jobsKey = acmx2::backend_settings_key(acmx2::Backend::Acmxvk, "parallel_build_jobs");
5135 auto *parallelBuildCheckBox = new QCheckBox(tr("Enable parallel build"), &dialog);
5136 auto *parallelBuildJobsSpinBox = new QSpinBox(&dialog);
5137 parallelBuildJobsSpinBox->setRange(1, 256);
5138 parallelBuildJobsSpinBox->setValue(qBound(1, settings.value(jobsKey, 2).toInt(), 256));
5139 parallelBuildCheckBox->setChecked(settings.value(enabledKey, false).toBool());
5140 parallelBuildJobsSpinBox->setEnabled(parallelBuildCheckBox->isChecked());
5141 parallelBuildJobsSpinBox->setToolTip(tr("Number of concurrent ACMXVK shader compiler jobs (1-256)."));
5142 auto *parallelBuildLayout = new QHBoxLayout();
5143 parallelBuildLayout->addWidget(parallelBuildCheckBox);
5144 parallelBuildLayout->addWidget(new QLabel(tr("Jobs:"), &dialog));
5145 parallelBuildLayout->addWidget(parallelBuildJobsSpinBox);
5146 parallelBuildLayout->addStretch(1);
5147 layout->addLayout(parallelBuildLayout);
5148 connect(parallelBuildCheckBox, &QCheckBox::toggled, &dialog, [parallelBuildJobsSpinBox, enabledKey](bool enabled) {
5149 parallelBuildJobsSpinBox->setEnabled(enabled);
5150 QSettings settings("LostSideDead");
5151 settings.setValue(enabledKey, enabled);
5152 });
5153 connect(parallelBuildJobsSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), &dialog, [jobsKey](int jobs) {
5154 QSettings settings("LostSideDead");
5155 settings.setValue(jobsKey, jobs);
5156 });
5157 }
5158
5159 QDialogButtonBox *buttonBox = new QDialogButtonBox(&dialog);
5160 QPushButton *copyButton = buttonBox->addButton(tr("Copy to Clipboard"), QDialogButtonBox::ActionRole);
5161 QPushButton *runButton = buttonBox->addButton(tr("Run"), QDialogButtonBox::ActionRole);
5162 QPushButton *okButton = buttonBox->addButton(QDialogButtonBox::Ok);
5163 layout->addWidget(buttonBox);
5164
5165 const auto saveExtraArguments = [this, extraArgumentsEdit]() {
5166 extra_arguments = extraArgumentsEdit->text().trimmed();
5167 QSettings settings("LostSideDead", "acmx2");
5168 settings.setValue("interface/extra_arguments", extra_arguments);
5169 };
5170 const auto editedCommand = [textBox, extraArgumentsEdit]() {
5171 QString command = textBox->toPlainText().trimmed();
5172 const QString extras = extraArgumentsEdit->text().trimmed();
5173 if (!extras.isEmpty()) {
5174 command += QLatin1Char(' ');
5175 command += extras;
5176 }
5177 return command;
5178 };
5179
5180 connect(copyButton, &QPushButton::clicked, &dialog, [saveExtraArguments, editedCommand, &dialog]() {
5181 saveExtraArguments();
5182 const QString copiedText = editedCommand();
5183 QClipboard *clipboard = QGuiApplication::clipboard();
5184 clipboard->setText(copiedText, QClipboard::Clipboard);
5185#ifdef __linux__
5186 if (clipboard->supportsSelection()) {
5187 clipboard->setText(copiedText, QClipboard::Selection);
5188 }
5189#endif
5190 QCoreApplication::processEvents();
5191 QMessageBox::information(&dialog, tr("Copied"), tr("Command copied to clipboard."));
5192 });
5193 connect(runButton, &QPushButton::clicked, &dialog, [this, saveExtraArguments, editedCommand, &dialog]() {
5194 if (process->state() != QProcess::NotRunning) {
5195 QMessageBox::information(&dialog, tr("Process Running"), tr("A process is already running. Please stop it first."));
5196 return;
5197 }
5198 saveExtraArguments();
5199 const QString cmdText = editedCommand();
5200 if (cmdText.isEmpty()) {
5201 QMessageBox::warning(&dialog, tr("Empty Command"), tr("The command is empty."));
5202 return;
5203 }
5204
5205 // Run the command verbatim through a shell so that env-var prefixes,
5206 // quoting, and PATH lookup behave exactly like pasting it into a
5207 // terminal. This avoids any ambiguity from re-parsing the line into
5208 // tokens and re-applying environment via QProcessEnvironment.
5209 process->setProcessEnvironment(QProcessEnvironment::systemEnvironment());
5210#ifdef Q_OS_WIN
5211 QString shell = qEnvironmentVariable("COMSPEC");
5212 if (shell.isEmpty())
5213 shell = "cmd.exe";
5214 QStringList shellArgs{"/C", cmdText};
5215#else
5216 QString shell = "/bin/sh";
5217 QStringList shellArgs{"-c", cmdText};
5218#endif
5219 Log("shell: " + cmdText + "<br>");
5221 beginOutputRunLog(cmdText);
5222 process->start(shell, shellArgs);
5223 if (!process->waitForStarted()) {
5224 appendOutputRunLog(tr("Failed to start: %1").arg(process->errorString()));
5225 finishOutputRunLog(-1, QProcess::CrashExit);
5226 Log("<b style='color:red;'>Failed to start the program.</b>");
5227 QMessageBox::critical(&dialog, tr("Error"), tr("Failed to start the program."));
5228 return;
5229 }
5230 play_stop->setEnabled(true);
5231 dialog.accept();
5232 });
5233 connect(okButton, &QPushButton::clicked, &dialog, [saveExtraArguments, &dialog]() {
5234 saveExtraArguments();
5235 dialog.accept();
5236 });
5237
5238 dialog.exec();
5239}
5240
5241QString MainWindow::concatList(const QStringList lst) {
5242 QString text;
5243 QTextStream stream(&text);
5244 for (auto &i : lst) {
5245 stream << i << " ";
5246 }
5247 return text;
5248}
5249
5251 QStringList indices;
5252 loadShaders(shader_path, true);
5253 for (const QString &name : shader_pass_names) {
5254 int idx = items.indexOf(name);
5255 if (idx >= 0) {
5256 indices.append(QString::number(idx));
5257 }
5258 }
5259 return indices.join(",");
5260}
5261
5262QString MainWindow::sanitizeShaderName(const QString &name) {
5263 QString sanitized = name.trimmed();
5264 sanitized.replace('\\', '/');
5265 sanitized = QDir::cleanPath(sanitized);
5266
5267 while (sanitized.startsWith("./")) {
5268 sanitized = sanitized.mid(2);
5269 }
5270
5271 if (sanitized.isEmpty() || sanitized == "." || sanitized == "..") {
5272 Log("Warning: Invalid shader name detected: " + name);
5273 return QString();
5274 }
5275
5276 if (QDir::isAbsolutePath(sanitized) || sanitized.startsWith("../") || sanitized.contains("/../") || sanitized.endsWith("/..")) {
5277 Log("Warning: Invalid shader name detected (path traversal attempt): " + name);
5278 return QString();
5279 }
5280
5281 return sanitized;
5282}
5283
5285 open_files.erase(std::remove_if(open_files.begin(), open_files.end(), [](const QPointer<TextEditor> &ptr) { return ptr.isNull(); }), open_files.end());
5286}
5287
5289 if (items.isEmpty()) {
5290 return;
5291 }
5292 std::random_device rd;
5293 std::mt19937 g(rd());
5294 std::shuffle(items.begin(), items.end(), g);
5296 updateIndex();
5297 Log("Shaders shuffled");
5298}
5299
5301 if (items.isEmpty()) {
5302 return;
5303 }
5304 items.sort(Qt::CaseInsensitive);
5306 updateIndex();
5307 Log("Shaders sorted alphabetically");
5308}
5309
5311 QString build_path = shader_path;
5312 if (build_path.isEmpty()) {
5313 QSettings appSettings("LostSideDead");
5314 build_path = appSettings.value(acmx2::backend_settings_key(active_backend, "library"), active_backend == acmx2::Backend::Acmx2 ? appSettings.value("shaders", "").toString() : QString()).toString();
5315 }
5316
5317 if (build_path.isEmpty()) {
5318 cacheBuildInProgress = false;
5319 QMessageBox::warning(this, "Error", "No shader library loaded. Please set a shader directory in Properties or load a shader library first.");
5320 return;
5321 }
5322
5323 if (process->state() == QProcess::Running) {
5324 cacheBuildInProgress = false;
5325 QMessageBox::warning(this, "Error", "A process is already running. Please wait for it to finish.");
5326 return;
5327 }
5328
5331 return;
5332 }
5333
5334#ifdef Q_OS_MACOS
5335 // ACMX2 does not support its persistent OpenGL binary cache on macOS.
5336 Log("Rebuild Shader Cache is not available on macOS.");
5337 cacheBuildInProgress = false;
5338 return;
5339#else
5340
5341 const QString assets_path = resolveAssetsPath();
5342
5343 QStringList args;
5344 args << "--build" << build_path;
5345 args << "-p" << assets_path;
5346 args << "--texture-cache-size" << QString::number(cache_size > 0 ? cache_size : 8);
5347 if (cache_enabled && textureCacheArraySettingEnabled())
5348 args << "--texture-cache-array";
5349
5350 if (enable_3d) {
5351 args << "--enable-3d";
5352 }
5353
5354 Log("Building shader cache for: " + build_path);
5355 Log("Command: " + executable_path + " " + args.join(" ") + "<br>");
5356
5357 play_stop->setEnabled(true);
5358 cacheBuildInProgress = true;
5360 process->start(executable_path, args);
5361
5362 if (!process->waitForStarted()) {
5363 Log("<b style='color:red;'>Error:</b> Failed to start shader cache build process");
5364 cacheBuildInProgress = false;
5365 play_stop->setEnabled(false);
5366 }
5367#endif
5368}
5369
5372 return;
5373 if (process->state() == QProcess::Running) {
5374 QMessageBox::warning(this, tr("Fix Build"), tr("A process is already running. Please wait for it to finish."));
5375 return;
5376 }
5377 if (shader_path.isEmpty()) {
5378 QMessageBox::warning(this, tr("Fix Build"), tr("No shader library is loaded."));
5379 return;
5380 }
5382}
5383
5384void MainWindow::start_acmxvk_build(const QString &build_path, AcmxvkBuildMode mode) {
5385 const bool fix = mode != AcmxvkBuildMode::Strict;
5386 const bool prune = mode == AcmxvkBuildMode::Prune;
5387 const QString dialogTitle = prune ? tr("Remove Broken Shaders") : (fix ? tr("Fix Build") : tr("Build ACMXVK Library"));
5388 QString type_error;
5389 if (!is_acmxvk_source_library(build_path, type_error)) {
5391 QMessageBox::warning(this,
5392 dialogTitle,
5393 type_error.isEmpty() ? tr("The selected ACMXVK library is already a compiled "
5394 "runtime library.")
5395 : type_error);
5396 return;
5397 }
5398
5399 const QString manifest_path = QDir(build_path).filePath(QStringLiteral("library.json"));
5400 if (!QFileInfo(manifest_path).isFile()) {
5402 QMessageBox::warning(this, dialogTitle, tr("ACMXVK source builds require library.json:\n%1").arg(manifest_path));
5403 return;
5404 }
5405
5406 const QString output_path = acmxvk_build_directory(build_path);
5407 QString compilerError;
5408 const QString compiler = resolve_acmxvk_shader_compiler(compilerError);
5409 if (compiler.isEmpty()) {
5411 Log(tr("<b style='color:red;'>Cannot build ACMXVK library: %1</b>").arg(compilerError.toHtmlEscaped()));
5412 QMessageBox::warning(this, tr("ACMXVK Shader Compiler"), compilerError);
5413 return;
5414 }
5415 QStringList arguments{"--unbuffered", "--build", manifest_path};
5416 arguments << (fix ? QStringLiteral("--fix") : QStringLiteral("--builddir")) << output_path;
5417 arguments << QStringLiteral("--glslc") << compiler;
5418 QSettings settings("LostSideDead");
5419 const bool parallelBuildEnabled = settings.value(acmx2::backend_settings_key(acmx2::Backend::Acmxvk, "parallel_build_enabled"), false).toBool();
5420 if (parallelBuildEnabled) {
5421 const int parallelBuildJobs = qBound(1, settings.value(acmx2::backend_settings_key(acmx2::Backend::Acmxvk, "parallel_build_jobs"), 2).toInt(), 256);
5422 arguments << QStringLiteral("--parallel") << QString::number(parallelBuildJobs);
5423 }
5424 if (prune)
5425 arguments << QStringLiteral("--prune") << QStringLiteral("--force");
5426 Log(prune ? tr("Removing broken ACMXVK shader sources from: %1").arg(build_path) : (fix ? tr("Fix building ACMXVK SPIR-V library: %1").arg(build_path) : tr("Building ACMXVK SPIR-V library: %1").arg(build_path)));
5427 Log("Command: " + executable_path + " " + concatList(arguments) + "<br>");
5428 play_stop->setEnabled(true);
5429 cacheBuildInProgress = true;
5430 acmxvkPruneLibraryPath = prune ? build_path : QString();
5431 process->start(executable_path, arguments);
5432 if (!process->waitForStarted()) {
5433 Log("<b style='color:red;'>Error:</b> Failed to start ACMXVK build process");
5434 cacheBuildInProgress = false;
5435 acmxvkPruneLibraryPath.clear();
5437 play_stop->setEnabled(false);
5438 }
5439}
5440
5442
5444 MetadataViewer dlg(this);
5445 dlg.exec();
5446}
5447
5449 QString scan_path = shader_path;
5450 if (scan_path.isEmpty()) {
5451 QSettings appSettings("LostSideDead");
5452 scan_path = appSettings.value(acmx2::backend_settings_key(active_backend, "library"), active_backend == acmx2::Backend::Acmx2 ? appSettings.value("shaders", "").toString() : QString()).toString();
5453 }
5454 if (scan_path.isEmpty()) {
5455 QMessageBox::warning(this, "Error", "No shader library loaded. Please set a shader directory in Properties or load a shader library first.");
5456 return;
5457 }
5458 if (process->state() == QProcess::Running) {
5459 QMessageBox::warning(this, "Error", "A process is already running. Please wait for it to finish.");
5460 return;
5461 }
5462
5463 const QString manifestPath = acmx2::shader_manifest_path(scan_path);
5464 if (manifestPath.isEmpty()) {
5465 QMessageBox::warning(this, "Missing Shader Manifest", "No library.json or index.txt found in: " + scan_path);
5466 return;
5467 }
5468 const QString manifestName = QFileInfo(manifestPath).fileName();
5469
5471 QString typeError;
5472 if (!is_acmxvk_source_library(scan_path, typeError)) {
5473 QMessageBox::warning(this,
5474 tr("Remove Broken Shaders"),
5475 typeError.isEmpty() ? tr("Remove Broken requires an ACMXVK source library, "
5476 "not a compiled runtime library.")
5477 : typeError);
5478 return;
5479 }
5480
5481 QMessageBox confirmation(this);
5482 confirmation.setIcon(QMessageBox::Warning);
5483 confirmation.setWindowTitle(tr("Permanently Remove Broken Shaders"));
5484 confirmation.setText(tr("This operation permanently deletes source shader files."));
5485 confirmation.setInformativeText(tr("ACMXVK will compile every shader listed in:\n\n%1\n\n"
5486 "Any .frag or .comp source for which glslc reports a compilation "
5487 "failure will be deleted. The generated runtime library and the "
5488 "source library manifest will then omit those shaders.\n\n"
5489 "No backup is created and this operation cannot be undone. "
5490 "Commit or archive the library before continuing.\n\n"
5491 "Do you want to permanently remove the broken sources?")
5492 .arg(scan_path));
5493 confirmation.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
5494 confirmation.setDefaultButton(QMessageBox::No);
5495 confirmation.setEscapeButton(QMessageBox::No);
5496 if (confirmation.exec() != QMessageBox::Yes)
5497 return;
5498
5500 return;
5501 }
5502
5503 QMessageBox::StandardButton reply = QMessageBox::question(this,
5504 tr("Remove Broken Shaders"),
5505 tr("This will compile every shader in:\n\n%1\n\n"
5506 "Any shader that fails to compile will be removed from %2 "
5507 "(the original will be backed up as %2.bak).\n\nContinue?")
5508 .arg(scan_path, manifestName),
5509 QMessageBox::Yes | QMessageBox::No);
5510 if (reply != QMessageBox::Yes)
5511 return;
5512
5513 const QString assets_path = resolveAssetsPath();
5514
5515 QStringList args;
5516 args << "--remove-broken" << scan_path;
5517 args << "-p" << assets_path;
5518 args << "--texture-cache-size" << QString::number(cache_size > 0 ? cache_size : 8);
5519 if (cache_enabled && textureCacheArraySettingEnabled())
5520 args << "--texture-cache-array";
5521 if (enable_3d)
5522 args << "--enable-3d";
5523
5524 Log("Scanning for broken shaders in: " + scan_path);
5525 Log("Command: " + executable_path + " " + args.join(" ") + "<br>");
5526
5527 // Use a dedicated QProcess so we can reload the list when it finishes
5528 // without interfering with the main playback process.
5529 QProcess *scan = new QProcess(this);
5530 scan->setProcessChannelMode(QProcess::SeparateChannels);
5531 connect(scan, &QProcess::readyReadStandardOutput, this, [this, scan]() {
5532 QString output = scan->readAllStandardOutput();
5533 output.replace("\n", "<br>");
5534 this->Write(output);
5535 });
5536 connect(scan, &QProcess::readyReadStandardError, this, [this, scan]() {
5537 QString output = scan->readAllStandardError();
5538 output.replace("\n", "<br>");
5539 this->Write("<b style='color:red;'>" + output + "</b>");
5540 });
5541 connect(scan, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, [this, scan, scan_path, manifestName](int exitCode, QProcess::ExitStatus) {
5542 Log(QString("Remove-broken finished with exit code: %1<br>").arg(exitCode));
5543 if (exitCode == 0) {
5544 // Reload the list view from the updated manifest.
5545 loadShaders(scan_path, true);
5546 QMessageBox::information(this,
5547 tr("Remove Broken"),
5548 tr("Finished scanning shader library.\n\n"
5549 "%1 has been updated and the shader list reloaded.\n"
5550 "A backup of the original is at:\n%2/%1.bak")
5551 .arg(manifestName, scan_path));
5552 } else {
5553 QMessageBox::warning(this,
5554 tr("Remove Broken"),
5555 tr("Remove-broken failed with exit code %1. "
5556 "%2 was not changed.")
5557 .arg(exitCode)
5558 .arg(manifestName));
5559 }
5560 scan->deleteLater();
5561 });
5562
5563 scan->start(executable_path, args);
5564 if (!scan->waitForStarted()) {
5565 Log("<b style='color:red;'>Error:</b> Failed to start remove-broken process");
5566 scan->deleteLater();
5567 }
5568}
5569
5571#ifdef Q_OS_MACOS
5572 Log("Clean Shader Cache is not available on macOS.");
5573 return;
5574#else
5575 QString libraryPath = shader_path;
5576 if (libraryPath.isEmpty()) {
5577 QSettings appSettings("LostSideDead");
5578 libraryPath = appSettings.value("shaders", "").toString();
5579 }
5580
5581 if (libraryPath.isEmpty()) {
5582 QMessageBox::warning(this, "Error", "No shader library loaded. Please set a shader directory in Properties or load a shader library first.");
5583 return;
5584 }
5585 if (process->state() == QProcess::Running || cacheBuildInProgress) {
5586 QMessageBox::warning(this, "Error", "A process is running. Stop it before cleaning the shader cache.");
5587 return;
5588 }
5589
5590 const QMessageBox::StandardButton reply = QMessageBox::question(this,
5591 tr("Clean Shader Cache"),
5592 tr("Delete all cached shader binaries for:\n\n%1\n\n"
5593 "This will not rebuild the cache. Continue?")
5594 .arg(libraryPath),
5595 QMessageBox::Yes | QMessageBox::No);
5596 if (reply != QMessageBox::Yes)
5597 return;
5598
5599 const QString assetsPath = resolveAssetsPath();
5600 QStringList cacheFiles;
5601 const auto addCacheFile = [&cacheFiles](const QString &path) {
5602 if (!cacheFiles.contains(path))
5603 cacheFiles.append(path);
5604 };
5605
5606 // Current cache files are keyed by texture-cache size and array mode.
5607 // Enumerate every valid combination so cleaning is independent of the
5608 // currently selected Session Settings.
5609 for (int size = 1; size <= 64; ++size) {
5610 for (const bool useArray : {false, true}) {
5611 const QString filename = shaderCacheFilename(libraryPath, size, useArray);
5612 addCacheFile(assetsPath + "/" + filename);
5613 addCacheFile(libraryPath + "/" + filename);
5614 }
5615 }
5616
5617 // Remove the pre-size-key hashed cache and the original fixed-name cache.
5618 std::error_code ec;
5619 const std::filesystem::path libraryFsPath(libraryPath.toStdString());
5620 const std::filesystem::path absoluteLibrary = std::filesystem::absolute(libraryFsPath, ec);
5621 const std::string legacyKey = ec ? libraryPath.toStdString() : absoluteLibrary.lexically_normal().string();
5622 std::ostringstream legacyName;
5623 legacyName << ".shader_cache_" << std::hex << std::hash<std::string>{}(legacyKey);
5624 const QString legacyHashedName = QString::fromStdString(legacyName.str());
5625 addCacheFile(assetsPath + "/" + legacyHashedName);
5626 addCacheFile(libraryPath + "/" + legacyHashedName);
5627 addCacheFile(libraryPath + "/.shader_cache");
5628
5629 int removedCount = 0;
5630 int failedCount = 0;
5631 for (const QString &cacheFile : cacheFiles) {
5632 if (!QFileInfo::exists(cacheFile))
5633 continue;
5634 if (QFile::remove(cacheFile)) {
5635 Log("Deleted shader cache: " + cacheFile);
5636 ++removedCount;
5637 } else {
5638 Log("<b style='color:red;'>Warning:</b> Could not delete cache file: " + cacheFile);
5639 ++failedCount;
5640 }
5641 }
5642
5643 if (removedCount == 0 && failedCount == 0) {
5644 Log("No existing shader cache found");
5645 } else {
5646 Log(QString("Shader cache clean complete: removed %1 file(s), %2 failed").arg(removedCount).arg(failedCount));
5647 }
5649#endif
5650}
5651
5653
5654static QString probe_feature_output(const QString &exe, const QString &flag) {
5655 QProcess probe;
5656 probe.start(exe, QStringList() << flag);
5657 if (!probe.waitForFinished(5000)) {
5658 probe.kill();
5659 return {};
5660 }
5661 return QString::fromLocal8Bit(probe.readAllStandardOutput()).trimmed();
5662}
5663
5664static bool probeFeature(const QString &exe, const QString &flag, const QString &token) { return probe_feature_output(exe, flag).contains(token, Qt::CaseInsensitive); }
5665
5667 const bool isAcmxvk = active_backend == acmx2::Backend::Acmxvk;
5668 const QString backendName = acmx2::backend_name(active_backend);
5669 const QString cudaOutput = probe_feature_output(executable_path, "--check-cuda");
5670 cuda_available = cudaOutput.contains(isAcmxvk ? "acidcam-gpu filters: enabled" : "CUDA: enabled", Qt::CaseInsensitive);
5671 cuda_device_available = isAcmxvk ? cudaOutput.contains("MXVK CUDA interop: enabled", Qt::CaseInsensitive) : cuda_available;
5672 audio_available = probeFeature(executable_path, "--check-audio", "AUDIO: enabled");
5673 midi_available = probeFeature(executable_path, "--check-midi", "MIDI: enabled");
5674 dnn_available = probeFeature(executable_path, "--check-dnn", isAcmxvk ? "OpenCV DNN effects: enabled" : "OpenCV DNN: enabled");
5675 deep_dream_available = isAcmxvk && probeFeature(executable_path, "--check-deep-dream", "Deep Dream: enabled");
5676 stable_diffusion_available = isAcmxvk && probeFeature(executable_path, "--check-stable-diffusion", "Stable Diffusion: enabled");
5677
5678 Log(QString("CUDA filters: %1 (%2)").arg(cuda_available ? "enabled" : "disabled", backendName));
5679 if (isAcmxvk) {
5680 Log(QString("CUDA device interop: %1 (%2)").arg(cuda_device_available ? "enabled" : "disabled", backendName));
5681 }
5682 Log(QString("AUDIO: %1 (%2)").arg(audio_available ? "enabled" : "disabled", backendName));
5683 Log(QString("MIDI: %1 (%2)").arg(midi_available ? "enabled" : "disabled", backendName));
5684 Log(QString("OpenCV DNN: %1 (%2)").arg(dnn_available ? "enabled" : "disabled", backendName));
5685 if (isAcmxvk) {
5686 Log(QString("Deep Dream: %1 (%2)").arg(deep_dream_available ? "enabled" : "disabled", backendName));
5687 Log(QString("Stable Diffusion: %1 (%2)").arg(stable_diffusion_available ? "enabled" : "disabled", backendName));
5688 }
5689
5690 if (!dnn_available) {
5691 onnx_model_enabled = false;
5692 onnx_model.clear();
5693 }
5694
5695 if (deepDreamAction) {
5696 deepDreamAction->setVisible(isAcmxvk);
5698 deepDreamAction->setToolTip(deep_dream_available ? QString() : tr("Disabled: ACMXVK was built without Deep Dream support."));
5699 }
5700 if (isAcmxvk && !deep_dream_available) {
5701 deep_dream_enabled = false;
5703 }
5705 stableDiffusionAction->setVisible(isAcmxvk);
5708 : tr("Disabled: ACMXVK was built without Stable Diffusion "
5709 "support."));
5710 }
5711 if (isAcmxvk && !stable_diffusion_available) {
5713 }
5714 if (gpuFilterAction) {
5715 gpuFilterAction->setEnabled(cuda_available);
5716 gpuFilterAction->setToolTip(cuda_available ? QString() : tr("Disabled: %1 was built without acidcam-gpu filter support.").arg(backendName));
5717 }
5718 if (!cuda_available) {
5719 gpu_filter_enabled = false;
5720 gpu_filter_indices.clear();
5722 cuda_device = 0;
5723 }
5724
5725 if (audioSet) {
5726 audioSet->setEnabled(audio_available);
5727 audioSet->setToolTip(audio_available ? QString() : tr("Disabled: %1 was built without audio support.").arg(backendName));
5728 }
5729 if (!audio_available) {
5730 audio_enabled = false;
5731 record_audio = false;
5732 audio_passthrough = false;
5733 audio_file.clear();
5734 audio_trunc = false;
5735 audio_repeat = false;
5736 }
5737
5738 if (midiSettingsAction) {
5740 midiSettingsAction->setToolTip(midi_available ? QString() : tr("Disabled: %1 was built without MIDI support.").arg(backendName));
5741 }
5742 if (!midi_available) {
5743 midi_enabled = false;
5744 midi_config_file.clear();
5745 midi_device = -1;
5746 }
5747}
static QString probe_feature_output(const QString &exe, const QString &flag)
static bool probeFeature(const QString &exe, const QString &flag, const QString &token)
Main launcher window for ACMX2/ACMXVK shader selection and execution.
#define VERSION_AUTHOR
#define VERSION_INFO
Qt6 dialog for configuring audio reactivity settings.
Dialog for configuring live and file-based audio reactivity options.
QString getAudioFilePath() const
Get the effective audio file or M3U playlist path.
int getNumberOfChannels() const
double getSensitivity() const
int getAudioBufferFrames() const
Number of spectrum history frames requested.
bool isAudioFileEnabled() const
Check whether a file or M3U audio source is selected and has a path.
int getInputDeviceIndex() const
bool isAudioReactivityEnabled() const
bool isRecordAudioEnabled() const
bool isAudioPassThroughEnabled() const
bool isAudioRepeatEnabled() const
Check whether file audio should restart when it reaches the end.
double getRecordVolume() const
int getOutputDeviceIndex() const
double getAudioWarmRate() const
Audio startup warmup rate in 1/sec.
bool isAudioTruncEnabled() const
Check whether "stop when audio ends" is selected.
bool isAudioBuffersEnabled() const
Check whether spectrum history buffers are enabled.
void uniformDefinitionsChanged()
const QList< acmx2::CustomUniformDefinition > & uniforms() const
bool setUniformValue(const QString &name, double value)
bool loadLibrary(const QString &directory, acmx2::Backend backend, QString *error=nullptr)
CustomUniformDialog(QWidget *parent=nullptr)
DeepDreamConfiguration configuration() const
DeepDreamSettingsDialog(bool gpu_filter_enabled, QWidget *parent=nullptr)
void resultActivated(const QString &filePath, int lineNumber, int columnNumber, int matchLength)
Emitted when the user opens one search result.
FindShaderDialog(const QString &shaderPath, QWidget *parent=nullptr)
Dialog that manages optional GPU filter list and buffer settings.
Definition gpufilter.hpp:31
bool isGPUFilterEnabled() const
Return whether GPU filtering is enabled.
QString getFilterArgument() const
Build CLI argument payload for the selected filter state.
int getBufferSize() const
Return frame-buffer depth used by filter chain.
GPUFilterDialog(const QString &executablePath, QWidget *parent=nullptr)
Definition gpufilter.cpp:11
void settingsApplied(bool enabled, const QString &filterArgument, int bufferSize)
Emitted when settings should be applied without closing the dialog.
void libraryExported(const QString &directory)
Emitted after a complete library has been exported successfully.
LibraryBuilderDialog(acmx2::Backend backend, QWidget *parent=nullptr)
Shader-library utility dialog.
QString getShaderPath()
Return folder chosen for shader-library indexing.
QVector< QPointer< TextEditor > > open_files
void prompt_acmxvk_rebuild(const QString &reason, PendingAcmxvkAction resume_action)
Offer to rebuild a stale or incomplete ACMXVK source library.
void listClicked(const QModelIndex &i)
Handle shader list selection changes.
QPointer< UniformReferenceDialog > uniformReferenceDialog
QString currentShaderName() const
Return the filename in the Name column for the current selection.
void applyCustomStyleSheet(bool enable)
Apply or remove the custom stylesheet override.
void menuToggleDisplayFilter(bool checked)
void handleSavedShader(const QString &filePath)
QActionGroup * backendActionGroup
void update_backend_ui()
Update title, actions, and status text for the active backend.
PendingAcmxvkAction pending_acmxvk_action
QHash< QString, bool > shaderCacheStatus
Cached map of shader stem -> failed flag for the current library.
void openShaderEditor(const QString &filePath, int lineNumber=1, int columnNumber=0, int matchLength=0)
Open or focus an editor for a shader source location.
void set_backend(acmx2::Backend backend, bool persist=true)
Select the active rendering backend and restore its paths.
QPointer< PlaylistDialog > playlistDialog
void updateIndex()
Refresh shader index metadata timestamp.
void queueAcmxvkLiveCompile(const QString &filePath)
void populateShaderTree()
Repopulate the shader tree widget from the current items list, recomputing Last Modified,...
void updateOpenEditorCompileStatus(const QString &sourcePath, bool pending, bool success=false, const QString &diagnostics=QString())
QString sanitizeShaderName(const QString &name)
Normalize shader names for filesystem/process safety.
QList< double > stable_diffusion_lora_multipliers
int currentShaderRow() const
Return the row index of the current selection, or -1.
QPointer< QDialog > shaderEditorWorkspace
void applyMainViewStyles(bool customStyleEnabled)
void appendDeepDreamArguments(QStringList &arguments) const
bool validateDeepDreamLaunch(QString &error) const
QStringList stable_diffusion_lora_files
CustomUniformDialog * customUniformDialog
bool validateStableDiffusionLaunch(QString &error) const
void publishAcmxvkCompiledShaderReload(const QString &sourcePath, const QString &runtimePath)
void Write(const QString &message)
Write raw text to the lower output pane.
void publishSelectedShaderIndexToRunningProcess()
bool cacheBuildInProgress
True while an ACMX2 cache rebuild or ACMXVK source build is running.
void Log(const QString &message)
Append timestamped text to the UI log output.
void refreshShaderCacheStatus()
Refresh shaderCacheStatus from the on-disk shader cache.
QList< QPair< QString, QStringList > > playlist_tree_data
QPointer< DeepDreamSettingsDialog > deepDreamSettingsDialog
void publishShaderReloadToRunningProcess(const QString &filePath)
void loadSessionSettings()
Restore persisted Session Settings into the launcher's runtime state.
QString readFileContents(const QString &filePath)
Read an entire text file into memory.
QString getShaderPassIndicesFromNames()
Map selected shader-pass names back to numeric indices.
void appendStableDiffusionArguments(QStringList &arguments) const
bool publishAcmx2EditorPreview(const QString &filePath, const QString &source)
void selectShaderRow(int row)
Select the row at row and scroll it into view.
void appendOutputRunLog(const QString &message)
QPointer< GPUFilterDialog > gpuFilterDialog
QPointer< ShaderPassDialog > shaderPassDialog
void addRecentLibrary(const QString &path)
Add a library directory to the persisted recent-libraries list.
void updateRecentLibrariesMenu()
Rebuild the File > Load Recent submenu from persisted settings.
void finishOutputRunLog(int exitCode, QProcess::ExitStatus exitStatus)
QPointer< StableDiffusionSettingsDialog > stableDiffusionSettingsDialog
void start_acmxvk_build(const QString &build_path, AcmxvkBuildMode mode)
Start a strict, failure-tolerant, or destructive ACMXVK build.
bool backend_launch_available() const
Return whether the active backend can be launched.
void initControls()
Build menus, actions, widgets, and signal wiring.
bool loadShaders(const QString &path, bool force=false)
Load shader names from index/cache for the provided path.
QStringList editorPreviewTemporaryFiles
bool buildRunArguments(QStringList &arguments, PendingAcmxvkAction resume_action=PendingAcmxvkAction::None, bool include_extra_arguments=true)
Build acmx2 command-line arguments from current UI state.
QPointer< LibraryBuilderDialog > libraryBuilderDialog
void queueAcmxvkEditorPreview(const QString &filePath, const QString &source)
bool loadLibraryPath(const QString &path)
Validate, load, persist, and remember a shader library directory.
void runHdr10Conversion()
Run ffmpeg to convert the just-produced acmx2 output (assumed HLG HDR) into HDR10 (HEVC NVENC,...
void beginOutputRunLog(const QString &command)
QDateTime shaderCacheMTime
Modification time of the shader cache file when last read.
QString concatList(const QStringList lst)
Join list items into a comma-separated argument string.
MIDI settings dialog used by the main launcher UI.
int getMidiDeviceIndex() const
Return selected MIDI input device index.
QString getMidiConfigFile() const
Return path to the active MIDI mapping config file.
bool isMidiEnabled() const
Return whether MIDI support is enabled.
Playlist editor dialog with tree nodes and ordered shader entries.
int getAutopilotFrames() const
Return frames-per-shader threshold for autopilot mode (minimum 4).
PlaylistDialog(const QStringList &shaderNames, acmx2::Backend backend, QWidget *parent=nullptr)
QStringList getSelectedShaderNames() const
Return flattened selection of shaders in current playlist.
bool isPlaylistEnabled() const
Return whether playlist mode is enabled.
QString getPlaylistFile() const
Return current playlist file path.
bool isAutopilotRandom() const
Return true when random autopilot timeout mode is enabled.
QList< QPair< QString, QStringList > > getPlaylistTree() const
Return tree representation as node-name and shader-list pairs.
Launcher properties dialog for executable/shader/screenshot paths.
Definition prop.hpp:26
QLineEdit * shaderCompilerPathLineEdit
Definition prop.hpp:46
QLineEdit * shaderDirLineEdit
Definition prop.hpp:43
QComboBox * shaderCompilerComboBox
Definition prop.hpp:45
QLineEdit * exePathLineEdit
Definition prop.hpp:42
QLineEdit * screenshotDirLineEdit
Definition prop.hpp:44
Dialog that collects camera, input source, output, and runtime options.
Definition settings.hpp:32
bool isSavingOutputLog() const
bool isGenerateEnabled() const
bool isDurationLimitEnabled() const
Return whether maximum duration limiting is enabled.
bool isCopyAudioEnabled() const
int getSelectedCameraIndex() const
bool isSavingToOutputVideoFile() const
bool isEncodeNoDrop() const
QSize getSelectedScreenResolution() const
QString getEncodeRateControl() const
bool is_rotate_enabled() const
Return whether input-frame rotation is enabled.
QString getEncodeParameters() const
bool isTextureCacheEnabled() const
bool isEncodeConstantFrameRate() const
float getTimeSpeed() const
double getMaxSizeLimit() const
Return configured maximum output size in MB.
bool isPngOutputEnabled() const
Return whether Write PNG mode is enabled.
QString getModelFile() const
QString getOutputVideoFile() const
int getCacheSize() const
QString getPngOutputDirectory() const
Return the directory used for ACMXVK PNG-sequence output.
QString getGraphicsFile() const
bool isUsingGraphicsFile() const
bool isEncodeFillPtsGaps() const
bool isConvertToHdr10Enabled() const
bool isUseSourceFpsEnabled() const
QString getEncodeBitrate() const
QSize getSelectedCameraResolution() const
QString getInputVideoFile() const
int getPngLevel() const
Return the selected ACMXVK PNG compression level.
int getEncodeCrf() const
bool isUseSourceAudioEnabled() const
bool isMaxSizeLimitEnabled() const
Return whether maximum output-size limiting is enabled.
QString get_rotation_mode() const
Return the selected rotation token for –rotate.
bool isOnnxModelEnabled() const
int getCameraFPS() const
double getDurationLimit() const
Return configured max run duration in seconds.
bool isEncodeRealtime() const
bool isMaximizeFpsEnabled() const
float getCrossFadeDuration() const
Return crossfade duration between shader transitions.
void setCudaAvailable(bool available)
Show or hide CUDA-specific controls based on availability.
QString getEncodeCodec() const
void setDnnAvailable(bool available)
Enable or disable ONNX controls based on OpenCV DNN support.
bool isFlipEnabled() const
Return whether flip mode is enabled.
int getSelectedCudaDevice() const
bool is3dEnabled() const
int getGenerateInterval() const
QString getOnnxModelFile() const
bool isUseYuvEnabled() const
int getCacheDelay() const
QString getEncodeTune() const
QString getEncodePreset() const
bool isFullscreen() const
bool isUsingInputVideoFile() const
New-shader dialog with optional starter template content.
Definition shader.hpp:24
void setShaderPath(const QString &path)
Set output directory used for generated shader files.
Definition shader.cpp:136
UI for selecting and ordering shader passes.
void shaderEditRequested(const QString &shaderName)
Request that a shader in the pass list be opened for editing.
QStringList getSelectedShaderNames() const
Return selected shader names in execution order.
bool isShaderPassEnabled() const
Return whether shader-pass mode is enabled.
void settingsApplied(bool enabled, const QStringList &selectedShaderNames)
ShaderPassDialog(const QStringList &shaderNames, QWidget *parent=nullptr)
StableDiffusionConfiguration configuration() const
StableDiffusionSettingsDialog(QWidget *parent=nullptr)
Modal shader editor dialog used by ACMX2.
Definition editor.hpp:131
TextEditor(QWidget *parent=nullptr)
Definition editor.cpp:887
void fileSaved(const QString &path)
Emitted after the editor successfully writes its contents to disk.
void previewRequested(const QString &path, const QString &source)
void revealLocation(int lineNumber, int columnNumber=0, int matchLength=0)
Move to a one-based line and select the requested source match.
Definition editor.cpp:904
void uniformValueChanged(const QString &name, double value)
void openFileRequested(const QString &path, int lineNumber)
void setFileName(const QString &filename)
Associate editor with a shader file path.
Definition editor.cpp:897
QString fileName() const
Return the file currently associated with this editor.
Definition editor.cpp:902
void setText(const QString &text)
Replace editor contents and reset displayed text.
Definition editor.cpp:889
UniformReferenceDialog(acmx2::Backend backend, QWidget *parent=nullptr)
QString inject_safety_counters(const QString &glsl_source)
Add bounded iteration guards and preserve original GLSL diagnostic lines.
Definition editor.cpp:107
Regular-expression search dialog for shader source libraries.
Dialog for assembling portable shader libraries.
Media metadata viewer dialog.
constexpr std::uint32_t kShaderSelectionMaxDreamLayer
constexpr std::uint32_t kShaderSelectionMagic
constexpr std::uint32_t kShaderSelectionMaxShaderName
constexpr std::uint32_t kShaderSelectionMaxPassCount
constexpr std::uint32_t kShaderSelectionMaxUniformName
constexpr std::uint32_t kShaderSelectionVersion
constexpr std::uint32_t kShaderSelectionMaxWatermarkText
constexpr std::uint32_t kShaderSelectionMaxReloadPath
constexpr const char * kShaderSelectionShmName
constexpr const char * kShaderSelectionSemaphoreName
constexpr std::uint32_t kShaderSelectionMaxGpuFilterCount
constexpr std::uint32_t kShaderSelectionMaxCustomUniforms
constexpr std::uint32_t kShaderSelectionMaxDreamModelPath
constexpr std::uint32_t kShaderSelectionMaxAudioFilePath
QString shader_manifest_path(const QString &directory)
Resolve library.json first, then fall back to index.txt.
bool custom_uniform_metadata_matches(const QString &leftDirectory, const QString &rightDirectory, bool &matches, QString &error)
Compare two manifests' custom-uniform ABI and numeric metadata.
QDateTime shader_manifest_last_modified(const QString &directory)
Return the selected manifest's modification time.
bool load_custom_uniforms(const QString &directory, QList< CustomUniformDefinition > &uniforms, QString &error)
Load custom float-uniform controls from library.json.
std::optional< ShaderLibraryType > shader_manifest_library_type(const QString &directory, QString &error)
Read and validate an optional top-level library_type value.
bool remove_shader_manifest_entry(const QString &directory, const QString &shader, QString &error)
Remove one shader from the preferred manifest, without deleting its file.
QString default_backend_executable(Backend backend)
Definition backend.hpp:16
Backend
Definition backend.hpp:8
bool isCustomStyleEnabled()
QString backend_settings_key(Backend backend, const QString &setting)
Definition backend.hpp:14
void applyCustomStyleIfEnabled(QWidget *widget)
bool shader_manifest_exists(const QString &directory)
Return true when either supported manifest exists.
bool load_shader_manifest(const QString &directory, QStringList &shaders, QString &error)
Load shader filenames from the preferred manifest in a directory.
std::optional< Backend > backend_from_id(const QString &value)
Definition backend.hpp:24
QString backend_id(Backend backend)
Definition backend.hpp:10
bool migrate_index_manifest_to_json(const QString &directory, bool &created, QString &error)
Create library.json from index.txt when a JSON manifest is absent.
QString backend_name(Backend backend)
Definition backend.hpp:12
bool write_shader_manifest(const QString &directory, const QStringList &shaders, QString &error)
Rewrite the preferred manifest, preserving JSON fields when possible.
QString defaultCustomStyleSheet()
QString buildStyleSheet(const CustomStylePalette &p)
std::optional< Backend > shader_manifest_backend(const QString &directory, QString &error)
Read an optional top-level backend hint from library.json.
QString acmxvk_build_directory(const QString &sourceLibrary)
bool acmxvk_runtime_manifest_matches(const QString &source_library, const QString &runtime_library, QString &error)
AcmxvkBuildState acmxvk_shader_build_state(const QString &source_library, const QString &source_name)
bool resolve_acmxvk_runtime_library(const QString &selectedLibrary, QString &runtimeLibrary, QString &error)
QString shaderCacheFilename(const QString &libraryPath, int cacheSize, bool useArray)
QString acmxvk_runtime_shader_name(const QString &sourceName)
QSize storedResolution(QSettings &settings, const QString &key, const QSize &fallback, bool defaultIsEmpty)
bool take_process_output_line(QString &buffer, QString &line)
QString buildShellCommand(const QStringList &envAssignments, const QString &program, const QStringList &arguments)
QString resolve_backend_assets_path(acmx2::Backend backend, const QString &executable, const QString &libraryPath)
bool is_acmxvk_source_library(const QString &libraryPath, QString &error)
QString resolveShaderCachePath(const QString &libraryPath, int cacheSize, bool useArray)
void replace_file(const std::filesystem::path &source, const std::filesystem::path &destination, std::error_code &error)
QHash< QString, bool > parseShaderCacheStatus(const QString &cachePath)
Main capture/playback settings dialog for ACMX2 execution.
Optional JSON and legacy text shader-library manifests.