ACMX 2.139.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
acmxvk-source-manifest.cpp
Go to the documentation of this file.
2
3#include <QDir>
4#include <QDirIterator>
5#include <QFile>
6#include <QFileInfo>
7#include <QHash>
8#include <QJsonArray>
9#include <QJsonDocument>
10#include <QJsonObject>
11#include <QJsonParseError>
12#include <QRegularExpression>
13#include <QSaveFile>
14#include <QSet>
15#include <QStringList>
16#include <algorithm>
17
18namespace {
20 double minimum = 0.0;
21 double maximum = 1.0;
22 double step = 0.01;
23 double value = 0.0;
24 };
25
26 QHash<int, QString> default_slot_names() {
27 return {{0, QStringLiteral("square_size")}, {1, QStringLiteral("alpha_value")}, {2, QStringLiteral("alpha_r")}, {3, QStringLiteral("alpha_g")}, {4, QStringLiteral("alpha_b")}, {5, QStringLiteral("value_alpha_r")}, {6, QStringLiteral("value_alpha_g")}, {7, QStringLiteral("value_alpha_b")}, {8, QStringLiteral("index_value")}, {9, QStringLiteral("restore_black")}, {10, QStringLiteral("seed")}, {11, QStringLiteral("value1")}, {12, QStringLiteral("iChannelTime")}, {13, QStringLiteral("time_speed")}, {14, QStringLiteral("blendAmt")}, {15, QStringLiteral("uDistortion")}, {16, QStringLiteral("uRotateSpeed")}, {17, QStringLiteral("uWarpSpeed")}, {18, QStringLiteral("uRandRate")}, {19, QStringLiteral("uPhaseRate")}, {20, QStringLiteral("slider1")}, {21, QStringLiteral("slider2")}, {22, QStringLiteral("slider3")}, {23, QStringLiteral("slider4")}, {24, QStringLiteral("frequency")}, {25, QStringLiteral("strength")}, {26, QStringLiteral("random_seed")}};
28 }
29
30 QHash<QString, UniformMetadata> default_metadata() {
31 QHash<QString, UniformMetadata> metadata;
32 metadata.insert(QStringLiteral("square_size"), {1.0, 128.0, 1.0, 55.0});
33 metadata.insert(QStringLiteral("slider1"), {0.0, 1.0, 0.01, 0.5});
34 metadata.insert(QStringLiteral("slider2"), {0.0, 1.0, 0.01, 0.6});
35 metadata.insert(QStringLiteral("slider3"), {0.0, 1.0, 0.01, 0.35});
36 metadata.insert(QStringLiteral("slider4"), {0.0, 1.0, 0.01, 0.8});
37 return metadata;
38 }
39
40 bool read_existing_metadata(const QString &path, QHash<QString, UniformMetadata> &metadata, QString &error) {
41 if (!QFileInfo::exists(path))
42 return true;
43
44 QFile file(path);
45 if (!file.open(QIODevice::ReadOnly)) {
46 error = QObject::tr("Cannot read existing manifest %1: %2").arg(path, file.errorString());
47 return false;
48 }
49 QJsonParseError parseError;
50 const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError);
51 if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
52 error = QObject::tr("Cannot parse existing manifest %1 at offset %2: %3").arg(path).arg(parseError.offset).arg(parseError.errorString());
53 return false;
54 }
55
56 const QJsonObject uniforms = document.object().value(QStringLiteral("custom_uniforms")).toObject();
57 for (auto it = uniforms.constBegin(); it != uniforms.constEnd(); ++it) {
58 if (!it.value().isObject())
59 continue;
60 const UniformMetadata fallback = metadata.value(it.key());
61 const QJsonObject value = it.value().toObject();
62 metadata.insert(it.key(), {value.value(QStringLiteral("minimum")).toDouble(fallback.minimum), value.value(QStringLiteral("maximum")).toDouble(fallback.maximum), value.value(QStringLiteral("step")).toDouble(fallback.step), value.value(QStringLiteral("value")).toDouble(fallback.value)});
63 }
64 return true;
65 }
66
67 bool scan_slots(const QString &path, QHash<int, QString> &slotNames, QHash<QString, int> &nameSlots, QString &error) {
68 QFile file(path);
69 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
70 error = QObject::tr("Cannot read %1: %2").arg(path, file.errorString());
71 return false;
72 }
73
74 static const QRegularExpression aliasExpression(QStringLiteral("^\\s*#define\\s+([A-Za-z_][A-Za-z0-9_]*)\\s+"
75 "ext\\.custom_uniforms\\[(\\d+)\\]\\.([xyzw])"));
76 const QStringList lines = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
77 for (const QString &line : lines) {
78 const QRegularExpressionMatch match = aliasExpression.match(line);
79 if (!match.hasMatch())
80 continue;
81 const QString name = match.captured(1);
82 const int vector = match.captured(2).toInt();
83 const int component = QStringLiteral("xyzw").indexOf(match.captured(3));
84 const int slot = vector * 4 + component;
85 if (slotNames.contains(slot) && slotNames.value(slot) != name) {
86 error = QObject::tr("Custom-uniform slot %1 is both %2 and %3 (%4)").arg(slot).arg(slotNames.value(slot), name, path);
87 return false;
88 }
89 if (nameSlots.contains(name) && nameSlots.value(name) != slot) {
90 error = QObject::tr("Custom uniform %1 uses slots %2 and %3 (%4)").arg(name).arg(nameSlots.value(name)).arg(slot).arg(path);
91 return false;
92 }
93 slotNames.insert(slot, name);
94 nameSlots.insert(name, slot);
95 }
96 return true;
97 }
98
99 bool shader_name_less(const QString &left, const QString &right) {
100 const int insensitive = QString::compare(left, right, Qt::CaseInsensitive);
101 return insensitive == 0 ? left < right : insensitive < 0;
102 }
103} // namespace
104
105namespace acmx2 {
106 static bool create_acmxvk_source_manifest_impl(const QString &rootDirectory, const QString &requestedOutputPath, const QStringList *includedShaders, AcmxvkSourceManifestResult &result, QString &error) {
107 result = {};
108 error.clear();
109
110 const QFileInfo rootInfo(rootDirectory);
111 if (!rootInfo.exists() || !rootInfo.isDir()) {
112 error = QObject::tr("Shader source directory does not exist: %1").arg(rootDirectory);
113 return false;
114 }
115 const QString root = QDir(rootInfo.absoluteFilePath()).canonicalPath();
116 const QString outputPath = requestedOutputPath.trimmed().isEmpty() ? QDir(root).filePath(QStringLiteral("library.json")) : QFileInfo(requestedOutputPath).absoluteFilePath();
117 if (!QDir().mkpath(QFileInfo(outputPath).absolutePath())) {
118 error = QObject::tr("Cannot create manifest directory: %1").arg(QFileInfo(outputPath).absolutePath());
119 return false;
120 }
121
122 QSet<QString> includedFiles;
123 if (includedShaders) {
124 for (QString entry : *includedShaders) {
125 entry = entry.trimmed();
126 entry.replace(QLatin1Char('\\'), QLatin1Char('/'));
127 entry = QDir::cleanPath(entry);
128 if (entry.isEmpty() || entry == QStringLiteral(".") || entry == QStringLiteral("..") || entry.startsWith(QStringLiteral("../")) || QDir::isAbsolutePath(entry)) {
129 error = QObject::tr("Invalid shader path in source manifest: %1").arg(entry);
130 return false;
131 }
132 if (includedFiles.contains(entry)) {
133 error = QObject::tr("Duplicate shader path in source manifest: %1").arg(entry);
134 return false;
135 }
136 includedFiles.insert(entry);
137 }
138 }
139
140 QHash<int, QString> slotNames = default_slot_names();
141 QHash<QString, int> nameSlots;
142 for (auto it = slotNames.constBegin(); it != slotNames.constEnd(); ++it)
143 nameSlots.insert(it.value(), it.key());
144
145 QStringList fragmentFiles;
146 QStringList computeFiles;
147 QSet<QString> discoveredFiles;
148 QDirIterator iterator(root, QDir::Files | QDir::Hidden, QDirIterator::Subdirectories);
149 while (iterator.hasNext()) {
150 const QString absolutePath = iterator.next();
151 QString relative = QDir(root).relativeFilePath(absolutePath);
152 relative.replace(QLatin1Char('\\'), QLatin1Char('/'));
153 const QStringList parts = relative.split(QLatin1Char('/'));
154 const int directoryPartCount = std::max(0, static_cast<int>(parts.size()) - 1);
155 const bool underCompute = parts.mid(0, directoryPartCount).contains(QStringLiteral("compute"));
156 const bool computeShader = underCompute && relative.endsWith(QStringLiteral(".comp"));
157 const bool fragmentShader = !underCompute && relative.endsWith(QStringLiteral(".frag"));
158 if (!computeShader && !fragmentShader)
159 continue;
160 if (includedShaders && !includedFiles.contains(relative))
161 continue;
162 if (computeShader) {
163 computeFiles.append(relative);
164 } else {
165 fragmentFiles.append(relative);
166 }
167 discoveredFiles.insert(relative);
168 if (!scan_slots(absolutePath, slotNames, nameSlots, error))
169 return false;
170 }
171
172 if (includedShaders) {
173 for (const QString &entry : includedFiles) {
174 if (!discoveredFiles.contains(entry)) {
175 error = QObject::tr("Manifest shader is missing or is not a supported "
176 ".frag/compute/*.comp source: %1")
177 .arg(entry);
178 return false;
179 }
180 }
181 }
182
183 std::sort(fragmentFiles.begin(), fragmentFiles.end(), shader_name_less);
184 std::sort(computeFiles.begin(), computeFiles.end(), shader_name_less);
185 QStringList shaders = fragmentFiles;
186 shaders.append(computeFiles);
187 if (shaders.isEmpty()) {
188 error = QObject::tr("No .frag or compute/*.comp sources found in %1").arg(root);
189 return false;
190 }
191 if (shaders.size() > 16384) {
192 error = QObject::tr("Shader library exceeds ACMXVK's 16384-entry limit");
193 return false;
194 }
195
196 QHash<QString, QString> outputNames;
197 for (const QString &shader : shaders) {
198 const QString compiled = (shader + QStringLiteral(".spv")).toLower();
199 if (outputNames.contains(compiled)) {
200 error = QObject::tr("Case-insensitive duplicate shader output: %1.spv").arg(shader);
201 return false;
202 }
203 outputNames.insert(compiled, shader);
204 }
205
206 int maximumSlot = 0;
207 for (auto it = slotNames.constBegin(); it != slotNames.constEnd(); ++it)
208 maximumSlot = std::max(maximumSlot, it.key());
209 for (int slot = 0; slot <= maximumSlot; ++slot) {
210 if (!slotNames.contains(slot)) {
211 error = QObject::tr("No custom-uniform name was found for slot %1").arg(slot);
212 return false;
213 }
214 }
215
216 QHash<QString, UniformMetadata> metadata = default_metadata();
217 if (!read_existing_metadata(outputPath, metadata, error))
218 return false;
219
220 QJsonObject customUniforms;
221 for (int slot = 0; slot <= maximumSlot; ++slot) {
222 const QString name = slotNames.value(slot);
223 const UniformMetadata values = metadata.value(name);
224 QJsonObject uniform;
225 uniform.insert(QStringLiteral("slot"), slot);
226 uniform.insert(QStringLiteral("minimum"), values.minimum);
227 uniform.insert(QStringLiteral("maximum"), values.maximum);
228 uniform.insert(QStringLiteral("step"), values.step);
229 uniform.insert(QStringLiteral("value"), values.value);
230 customUniforms.insert(name, uniform);
231 }
232 QJsonArray shaderArray;
233 for (const QString &shader : shaders)
234 shaderArray.append(shader);
235
236 QJsonObject manifest;
237 manifest.insert(QStringLiteral("version"), 1);
238 manifest.insert(QStringLiteral("backend"), QStringLiteral("acmxvk"));
239 manifest.insert(QStringLiteral("library_type"), QStringLiteral("source"));
240 manifest.insert(QStringLiteral("custom_uniforms"), customUniforms);
241 manifest.insert(QStringLiteral("shaders"), shaderArray);
242
243 QSaveFile output(outputPath);
244 if (!output.open(QIODevice::WriteOnly)) {
245 error = QObject::tr("Cannot write %1: %2").arg(outputPath, output.errorString());
246 return false;
247 }
248 const QByteArray json = QJsonDocument(manifest).toJson(QJsonDocument::Indented);
249 if (output.write(json) != json.size() || !output.commit()) {
250 error = QObject::tr("Cannot replace %1: %2").arg(outputPath, output.errorString());
251 return false;
252 }
253
254 result.fragmentCount = fragmentFiles.size();
255 result.computeCount = computeFiles.size();
256 result.customUniformCount = maximumSlot + 1;
257 result.outputPath = outputPath;
258 return true;
259 }
260
261 bool create_acmxvk_source_manifest(const QString &rootDirectory, const QString &outputPath, AcmxvkSourceManifestResult &result, QString &error) { return create_acmxvk_source_manifest_impl(rootDirectory, outputPath, nullptr, result, error); }
262
263 bool create_acmxvk_source_manifest_for_shaders(const QString &rootDirectory, const QStringList &shaderFiles, const QString &outputPath, AcmxvkSourceManifestResult &result, QString &error) { return create_acmxvk_source_manifest_impl(rootDirectory, outputPath, &shaderFiles, result, error); }
264} // namespace acmx2
bool create_acmxvk_source_manifest_for_shaders(const QString &rootDirectory, const QStringList &shaderFiles, const QString &outputPath, AcmxvkSourceManifestResult &result, QString &error)
Generate a source manifest from an explicit subset of files below root.
bool create_acmxvk_source_manifest(const QString &rootDirectory, const QString &outputPath, AcmxvkSourceManifestResult &result, QString &error)
Scan an ACMXVK source tree and atomically generate its library.json.
static bool create_acmxvk_source_manifest_impl(const QString &rootDirectory, const QString &requestedOutputPath, const QStringList *includedShaders, AcmxvkSourceManifestResult &result, QString &error)
QHash< QString, UniformMetadata > default_metadata()
bool scan_slots(const QString &path, QHash< int, QString > &slotNames, QHash< QString, int > &nameSlots, QString &error)
bool shader_name_less(const QString &left, const QString &right)
bool read_existing_metadata(const QString &path, QHash< QString, UniformMetadata > &metadata, QString &error)