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