| 1 | // Copyright (C) 2020 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
| 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 | |