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
21constexpr int DEAFULT_SEARCH_DEPTH = 0x10;
22
23static 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
45static 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
73struct SearchDepthGuard
74{
75 explicit SearchDepthGuard(int m) : max(m) {}
76 int value = 0;
77 const int max = DEAFULT_SEARCH_DEPTH;
78};
79
80static 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
102int 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 extractQsbFileOption({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 ExtractWhat : 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

source code of qtquick3d/tools/shadergen/main.cpp