1 | // Copyright (C) 2020 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include <QCoreApplication> |
5 | |
6 | #include <QXmlStreamWriter> |
7 | #include <qlist.h> |
8 | |
9 | #include <QCommandLineOption> |
10 | #include <QCommandLineParser> |
11 | |
12 | #include <QtCore/qfile.h> |
13 | #include <QtCore/qdir.h> |
14 | |
15 | #include <QtQuick3DUtils/private/qqsbcollection_p.h> |
16 | |
17 | #include "genshaders.h" |
18 | |
19 | #include "parser.h" |
20 | |
21 | constexpr int DEAFULT_SEARCH_DEPTH = 0x10; |
22 | |
23 | static int generateShaders(QVector<QString> &qsbcFiles, |
24 | const QVector<QString> &filePaths, |
25 | const QDir &sourceDir, |
26 | const QDir &outDir, |
27 | bool multilight, |
28 | bool verboseOutput, |
29 | bool dryRun) |
30 | { |
31 | MaterialParser::SceneData sceneData; |
32 | if (MaterialParser::parseQmlFiles(filePaths, sourceDir, sceneData, verboseOutput) == 0) { |
33 | if (sceneData.hasData()) { |
34 | GenShaders genShaders; |
35 | if (!genShaders.process(sceneData, qsbcFiles, outDir, generateMultipleLights: multilight, dryRun)) |
36 | return -1; |
37 | } else if (verboseOutput) { |
38 | if (!sceneData.viewport) |
39 | qWarning() << "No View3D item found" ; |
40 | } |
41 | } |
42 | return 0; |
43 | } |
44 | |
45 | static int writeResourceFile(const QString &resourceFile, |
46 | const QVector<QString> &qsbcFiles, |
47 | const QDir &outDir) |
48 | { |
49 | if (qsbcFiles.isEmpty()) |
50 | return -1; |
51 | |
52 | const QString outFilename = outDir.canonicalPath() + QDir::separator() + resourceFile; |
53 | QFile outFile(outFilename); |
54 | if (!outFile.open(flags: QFile::WriteOnly | QFile::Text | QFile::Truncate)) { |
55 | qWarning() << "Unable to create output file " << outFilename; |
56 | return -1; |
57 | } |
58 | |
59 | QXmlStreamWriter writer(&outFile); |
60 | writer.setAutoFormatting(true); |
61 | writer.writeStartElement(qualifiedName: "RCC" ); |
62 | writer.writeStartElement(qualifiedName: "qresource" ); |
63 | writer.writeAttribute(qualifiedName: "prefix" , value: "/" ); |
64 | for (const auto &f : qsbcFiles) |
65 | writer.writeTextElement(qualifiedName: "file" , text: f); |
66 | writer.writeEndElement(); |
67 | writer.writeEndElement(); |
68 | outFile.close(); |
69 | |
70 | return 0; |
71 | } |
72 | |
73 | struct SearchDepthGuard |
74 | { |
75 | explicit SearchDepthGuard(int m) : max(m) {} |
76 | int value = 0; |
77 | const int max = DEAFULT_SEARCH_DEPTH; |
78 | }; |
79 | |
80 | static void collectQmlFiles(const QList<QString> &pathArgs, QSet<QString> &filePaths, SearchDepthGuard &depth) |
81 | { |
82 | QFileInfo fi; |
83 | QDir dir; |
84 | for (const auto &arg : pathArgs) { |
85 | fi.setFile(arg); |
86 | if (fi.isFile()) { |
87 | if (fi.suffix() == QLatin1String("qml" )) |
88 | filePaths.insert(value: fi.canonicalFilePath()); |
89 | } else if (fi.isDir() && depth.value <= depth.max) { |
90 | dir.setPath(fi.filePath()); |
91 | const auto entries = dir.entryList(filters: QDir::Filter::Dirs | QDir::Filter::Files | QDir::Filter::NoDotAndDotDot); |
92 | const QString currentPath = QDir::currentPath(); |
93 | QDir::setCurrent(dir.path()); |
94 | ++depth.value; |
95 | collectQmlFiles(pathArgs: entries, filePaths, depth); |
96 | --depth.value; |
97 | QDir::setCurrent(currentPath); |
98 | } |
99 | } |
100 | } |
101 | |
102 | int main(int argc, char *argv[]) |
103 | { |
104 | QCoreApplication a(argc, argv); |
105 | |
106 | QCommandLineParser cmdLineparser; |
107 | cmdLineparser.setApplicationDescription("Pre-generates material shaders for Qt Quick 3D" ); |
108 | cmdLineparser.addHelpOption(); |
109 | // File options |
110 | QCommandLineOption changeDirOption({QChar(u'C'), QLatin1String("directory" )}, |
111 | QLatin1String("Change the working directory" ), |
112 | QLatin1String("dir" )); |
113 | cmdLineparser.addOption(commandLineOption: changeDirOption); |
114 | |
115 | // Debug options |
116 | QCommandLineOption verboseOutputOption({QChar(u'v'), QLatin1String("verbose" )}, QLatin1String("Turn on verbose output." )); |
117 | cmdLineparser.addOption(commandLineOption: verboseOutputOption); |
118 | |
119 | // Generator options |
120 | QCommandLineOption dryRunOption({QChar(u'n'), QLatin1String("dry-run" )}, QLatin1String("Runs as normal, but no files are created." )); |
121 | cmdLineparser.addOption(commandLineOption: dryRunOption); |
122 | |
123 | QCommandLineOption outputDirOption({QChar(u'o'), QLatin1String("output-dir" )}, QLatin1String("Output directory for generated files." ), QLatin1String("file" )); |
124 | cmdLineparser.addOption(commandLineOption: outputDirOption); |
125 | |
126 | QCommandLineOption resourceFileOption({QChar(u'r'), QLatin1String("resource-file" )}, QLatin1String("Name of generated resource file." ), QLatin1String("file" )); |
127 | cmdLineparser.addOption(commandLineOption: resourceFileOption); |
128 | |
129 | QCommandLineOption dumpQsbcFileOption({QChar(u'l'), QLatin1String("list-qsbc" )}, QLatin1String("Lists qsbc file content." )); |
130 | cmdLineparser.addOption(commandLineOption: dumpQsbcFileOption); |
131 | |
132 | QCommandLineOption ({QChar(u'e'), QLatin1String("extract-qsb" )}, QLatin1String("Extract qsb from collection." ), QLatin1String("key:[desc|vert|frag]" )); |
133 | cmdLineparser.addOption(commandLineOption: extractQsbFileOption); |
134 | |
135 | QCommandLineOption dirDepthOption(QLatin1String("depth" ), QLatin1String("Override default max depth (16) value when traversing the filesystem." ), QLatin1String("number" )); |
136 | cmdLineparser.addOption(commandLineOption: dirDepthOption); |
137 | |
138 | cmdLineparser.process(app: a); |
139 | |
140 | if (cmdLineparser.isSet(option: changeDirOption)) { |
141 | const auto value = cmdLineparser.value(option: changeDirOption); |
142 | QFileInfo fi(value); |
143 | if (!fi.isDir()) { |
144 | qWarning(msg: "%s : %s - Not a directory" , qPrintable(a.applicationName()), qPrintable(value)); |
145 | return -1; |
146 | } |
147 | QDir::setCurrent(value); |
148 | } |
149 | |
150 | QSet<QString> filePaths; |
151 | auto args = cmdLineparser.positionalArguments(); |
152 | |
153 | const bool collectQmlFilesMode = !(cmdLineparser.isSet(option: dumpQsbcFileOption) || cmdLineparser.isSet(option: extractQsbFileOption)); |
154 | if (collectQmlFilesMode) { |
155 | if (args.isEmpty()) |
156 | args.push_back(t: QDir::currentPath()); |
157 | |
158 | int searchDepth = DEAFULT_SEARCH_DEPTH; |
159 | if (cmdLineparser.isSet(option: dirDepthOption)) { |
160 | bool ok = false; |
161 | const int v = cmdLineparser.value(option: dirDepthOption).toInt(ok: &ok); |
162 | if (ok) |
163 | searchDepth = v; |
164 | } |
165 | |
166 | SearchDepthGuard depth(searchDepth); |
167 | collectQmlFiles(pathArgs: args, filePaths, depth); |
168 | } else if (!args.isEmpty()) { |
169 | filePaths.insert(value: args.first()); |
170 | } |
171 | |
172 | if (filePaths.isEmpty()) { |
173 | qWarning(msg: "No input file(s) found!" ); |
174 | a.exit(retcode: -1); |
175 | return -1; |
176 | } |
177 | |
178 | if (cmdLineparser.isSet(option: dumpQsbcFileOption)) { |
179 | const auto &f = *filePaths.cbegin(); |
180 | if (!f.isEmpty()) { |
181 | QQsbIODeviceCollection::dumpInfo(device: f); |
182 | a.exit(retcode: 0); |
183 | return 0; |
184 | } |
185 | } |
186 | |
187 | static const auto printBytes = [](const QByteArray &ba) { |
188 | for (const auto &b : ba) |
189 | printf(format: "%c" , b); |
190 | }; |
191 | |
192 | if (cmdLineparser.isSet(option: extractQsbFileOption)) { |
193 | const auto &f = *filePaths.cbegin(); |
194 | const auto k = cmdLineparser.value(option: extractQsbFileOption); |
195 | const auto kl = QStringView(k).split(sep: u':'); |
196 | |
197 | const auto &keyView = kl.at(i: 0); |
198 | const QByteArray key = keyView.toLatin1(); |
199 | enum : quint8 { Desc = 0x1, Vert = 0x2, Frag = 0x4 }; |
200 | quint8 what = 0; |
201 | if (kl.size() > 1) { |
202 | const auto &rest = kl.at(i: 1); |
203 | const auto &options = rest.split(sep: u'|'); |
204 | for (const auto &o : options) { |
205 | if (o == QLatin1String("desc" )) |
206 | what |= ExtractWhat::Desc; |
207 | if (o == QLatin1String("vert" )) |
208 | what |= ExtractWhat::Vert; |
209 | if (o == QLatin1String("frag" )) |
210 | what |= ExtractWhat::Frag; |
211 | } |
212 | } |
213 | QQsbIODeviceCollection qsbc(f); |
214 | if (qsbc.map(mode: QQsbIODeviceCollection::Read)) { |
215 | const auto entries = qsbc.availableEntries(); |
216 | const auto foundIt = entries.constFind(value: QQsbCollection::Entry(key)); |
217 | if (foundIt != entries.cend()) { |
218 | QQsbCollection::EntryDesc ed; |
219 | qsbc.extractEntry(entry: *foundIt, entryDesc&: ed); |
220 | if (what == 0) |
221 | qDebug(msg: "Entry with key %s found." , key.constData()); |
222 | if (what & ExtractWhat::Desc) |
223 | printBytes(ed.materialKey); |
224 | if (what & ExtractWhat::Vert) |
225 | printBytes(qUncompress(data: ed.vertShader.serialized())); |
226 | if (what & ExtractWhat::Frag) |
227 | printBytes(qUncompress(data: ed.fragShader.serialized())); |
228 | } else { |
229 | qWarning(msg: "Entry with key %s could not be found." , key.constData()); |
230 | } |
231 | qsbc.unmap(); |
232 | } |
233 | a.exit(retcode: 0); |
234 | return 0; |
235 | |
236 | qWarning(msg: "Command %s failed with input: %s and %s." , qPrintable(extractQsbFileOption.valueName()), qPrintable(f), qPrintable(k)); |
237 | a.exit(retcode: -1); |
238 | return -1; |
239 | } |
240 | |
241 | QString resourceFile = cmdLineparser.value(option: resourceFileOption); |
242 | if (resourceFile.isEmpty()) |
243 | resourceFile = QStringLiteral("genshaders.qrc" ); |
244 | |
245 | const bool dryRun = cmdLineparser.isSet(option: dryRunOption); |
246 | const QString &outputPath = cmdLineparser.isSet(option: outputDirOption) ? cmdLineparser.value(option: outputDirOption) : QDir::currentPath(); |
247 | QDir outDir; |
248 | if (!outputPath.isEmpty() && !dryRun) { |
249 | outDir.setPath(outputPath); |
250 | if (outDir.exists(name: outputPath) || (!outDir.exists(name: outputPath) && outDir.mkpath(dirPath: outputPath))) { |
251 | outDir.setPath(outputPath); |
252 | qDebug(msg: "Writing files to %s" , qPrintable(outDir.canonicalPath())); |
253 | } else { |
254 | qDebug(msg: "Unable to change or create output folder %s" , qPrintable(outputPath)); |
255 | return -1; |
256 | } |
257 | } |
258 | |
259 | const bool verboseOutput = cmdLineparser.isSet(option: verboseOutputOption); |
260 | const bool multilight = false; |
261 | |
262 | QVector<QString> qsbcFiles; |
263 | |
264 | int ret = 0; |
265 | if (filePaths.size()) |
266 | ret = generateShaders(qsbcFiles, filePaths: filePaths.values(), sourceDir: QDir::currentPath(), outDir, multilight, verboseOutput, dryRun); |
267 | |
268 | if (ret == 0 && !dryRun) |
269 | writeResourceFile(resourceFile, qsbcFiles, outDir); |
270 | |
271 | a.exit(retcode: ret); |
272 | return ret; |
273 | } |
274 | |