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