| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qspirvcompiler_p.h" |
| 5 | #include "qshaderrewriter_p.h" |
| 6 | #include <QFile> |
| 7 | #include <QFileInfo> |
| 8 | |
| 9 | QT_WARNING_PUSH |
| 10 | QT_WARNING_DISABLE_GCC("-Wsuggest-override" ) |
| 11 | #include <glslang/Public/ShaderLang.h> |
| 12 | #include <glslang/Public/ResourceLimits.h> |
| 13 | #include <SPIRV/GlslangToSpv.h> |
| 14 | QT_WARNING_POP |
| 15 | |
| 16 | //#define TOKENIZER_DEBUG |
| 17 | |
| 18 | QT_BEGIN_NAMESPACE |
| 19 | |
| 20 | struct QSpirvCompilerPrivate |
| 21 | { |
| 22 | bool readFile(const QString &fn); |
| 23 | bool compile(); |
| 24 | |
| 25 | QString sourceFileName; |
| 26 | QByteArray source; |
| 27 | QByteArray batchableSource; |
| 28 | EShLanguage stage = EShLangVertex; |
| 29 | QSpirvCompiler::Flags flags; |
| 30 | QByteArray preamble; |
| 31 | int batchAttrLoc = 7; |
| 32 | QByteArray spirv; |
| 33 | QString log; |
| 34 | }; |
| 35 | |
| 36 | bool QSpirvCompilerPrivate::readFile(const QString &fn) |
| 37 | { |
| 38 | QFile f(fn); |
| 39 | if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { |
| 40 | qWarning(msg: "QSpirvCompiler: Failed to open %s" , qPrintable(fn)); |
| 41 | return false; |
| 42 | } |
| 43 | source = f.readAll(); |
| 44 | batchableSource.clear(); |
| 45 | sourceFileName = fn; |
| 46 | f.close(); |
| 47 | return true; |
| 48 | } |
| 49 | |
| 50 | using namespace QtShaderTools; |
| 51 | |
| 52 | class Includer : public glslang::TShader::Includer |
| 53 | { |
| 54 | public: |
| 55 | IncludeResult *includeLocal(const char *, |
| 56 | const char *includerName, |
| 57 | size_t inclusionDepth) override |
| 58 | { |
| 59 | Q_UNUSED(inclusionDepth); |
| 60 | return readFile(headerName, includerName); |
| 61 | } |
| 62 | |
| 63 | IncludeResult *includeSystem(const char *, |
| 64 | const char *includerName, |
| 65 | size_t inclusionDepth) override |
| 66 | { |
| 67 | Q_UNUSED(inclusionDepth); |
| 68 | return readFile(headerName, includerName); |
| 69 | } |
| 70 | |
| 71 | void releaseInclude(IncludeResult *result) override |
| 72 | { |
| 73 | if (result) { |
| 74 | delete static_cast<QByteArray *>(result->userData); |
| 75 | delete result; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | private: |
| 80 | IncludeResult *readFile(const char *, const char *includerName); |
| 81 | }; |
| 82 | |
| 83 | glslang::TShader::Includer::IncludeResult *Includer::readFile(const char *, const char *includerName) |
| 84 | { |
| 85 | // Just treat the included name as relative to the includer: |
| 86 | // Take the path from the includer, append the included name, remove redundancies. |
| 87 | // This should work also for qrc (source filenames with qrc:/ or :/ prefix). |
| 88 | |
| 89 | QString includer = QString::fromUtf8(utf8: includerName); |
| 90 | if (includer.isEmpty()) |
| 91 | includer = QLatin1String("." ); |
| 92 | QString included = QFileInfo(includer).canonicalPath() + QLatin1Char('/') + QString::fromUtf8(utf8: headerName); |
| 93 | included = QFileInfo(included).canonicalFilePath(); |
| 94 | if (included.isEmpty()) { |
| 95 | qWarning(msg: "QSpirvCompiler: Failed to find include file %s" , headerName); |
| 96 | return nullptr; |
| 97 | } |
| 98 | QFile f(included); |
| 99 | if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { |
| 100 | qWarning(msg: "QSpirvCompiler: Failed to read include file %s" , qPrintable(included)); |
| 101 | return nullptr; |
| 102 | } |
| 103 | |
| 104 | QByteArray *data = new QByteArray; |
| 105 | *data = f.readAll(); |
| 106 | return new IncludeResult(included.toStdString(), data->constData(), data->size(), data); |
| 107 | } |
| 108 | |
| 109 | class GlobalInit |
| 110 | { |
| 111 | public: |
| 112 | GlobalInit() { glslang::InitializeProcess(); } |
| 113 | ~GlobalInit() { glslang::FinalizeProcess(); } |
| 114 | }; |
| 115 | |
| 116 | bool QSpirvCompilerPrivate::compile() |
| 117 | { |
| 118 | log.clear(); |
| 119 | |
| 120 | const bool useBatchable = (stage == EShLangVertex && flags.testFlag(flag: QSpirvCompiler::RewriteToMakeBatchableForSG)); |
| 121 | const QByteArray *actualSource = useBatchable ? &batchableSource : &source; |
| 122 | if (actualSource->isEmpty()) |
| 123 | return false; |
| 124 | |
| 125 | static GlobalInit globalInit; |
| 126 | |
| 127 | glslang::TShader shader(stage); |
| 128 | const QByteArray fn = sourceFileName.toUtf8(); |
| 129 | const char *fnStr = fn.constData(); |
| 130 | const char *srcStr = actualSource->constData(); |
| 131 | const int size = actualSource->size(); |
| 132 | shader.setStringsWithLengthsAndNames(s: &srcStr, l: &size, names: &fnStr, n: 1); |
| 133 | if (!preamble.isEmpty()) { |
| 134 | // Line numbers in errors and #version are not affected by having a |
| 135 | // preamble, which is just what we need. |
| 136 | shader.setPreamble(preamble.constData()); |
| 137 | } |
| 138 | |
| 139 | shader.setEnvInput(lang: glslang::EShSourceGlsl, envStage: stage, client: glslang::EShClientVulkan, version: 100); |
| 140 | shader.setEnvClient(client: glslang::EShClientVulkan, version: glslang::EShTargetVulkan_1_0); |
| 141 | shader.setEnvTarget(lang: glslang::EshTargetSpv, version: glslang::EShTargetSpv_1_0); |
| 142 | |
| 143 | int messages = EShMsgDefault; |
| 144 | if (flags.testFlag(flag: QSpirvCompiler::FullDebugInfo)) // embed source |
| 145 | messages |= EShMsgDebugInfo; |
| 146 | |
| 147 | Includer includer; |
| 148 | if (!shader.parse(builtInResources: GetDefaultResources(), defaultVersion: 100, forwardCompatible: false, messages: EShMessages(messages), includer)) { |
| 149 | qWarning(msg: "QSpirvCompiler: Failed to parse shader" ); |
| 150 | log = QString::fromUtf8(utf8: shader.getInfoLog()).trimmed(); |
| 151 | return false; |
| 152 | } |
| 153 | |
| 154 | glslang::TProgram program; |
| 155 | program.addShader(shader: &shader); |
| 156 | if (!program.link(EShMsgDefault)) { |
| 157 | qWarning(msg: "QSpirvCompiler: Link failed" ); |
| 158 | log = QString::fromUtf8(utf8: shader.getInfoLog()).trimmed(); |
| 159 | return false; |
| 160 | } |
| 161 | |
| 162 | // The only interesting option here is the debug info, optimizations and |
| 163 | // such do not happen at this level. |
| 164 | glslang::SpvOptions options; |
| 165 | options.generateDebugInfo = flags.testFlag(flag: QSpirvCompiler::FullDebugInfo); |
| 166 | |
| 167 | std::vector<unsigned int> spv; |
| 168 | glslang::GlslangToSpv(intermediate: *program.getIntermediate(stage), spirv&: spv, options: &options); |
| 169 | if (!spv.size()) { |
| 170 | qWarning(msg: "Failed to generate SPIR-V" ); |
| 171 | return false; |
| 172 | } |
| 173 | |
| 174 | spirv.resize(size: int(spv.size() * 4)); |
| 175 | memcpy(dest: spirv.data(), src: spv.data(), n: spirv.size()); |
| 176 | |
| 177 | return true; |
| 178 | } |
| 179 | |
| 180 | QSpirvCompiler::QSpirvCompiler() |
| 181 | : d(new QSpirvCompilerPrivate) |
| 182 | { |
| 183 | } |
| 184 | |
| 185 | QSpirvCompiler::~QSpirvCompiler() |
| 186 | { |
| 187 | delete d; |
| 188 | } |
| 189 | |
| 190 | void QSpirvCompiler::setSourceFileName(const QString &fileName) |
| 191 | { |
| 192 | if (!d->readFile(fn: fileName)) |
| 193 | return; |
| 194 | |
| 195 | const QString suffix = QFileInfo(fileName).suffix(); |
| 196 | if (suffix == QStringLiteral("vert" )) { |
| 197 | d->stage = EShLangVertex; |
| 198 | } else if (suffix == QStringLiteral("frag" )) { |
| 199 | d->stage = EShLangFragment; |
| 200 | } else if (suffix == QStringLiteral("tesc" )) { |
| 201 | d->stage = EShLangTessControl; |
| 202 | } else if (suffix == QStringLiteral("tese" )) { |
| 203 | d->stage = EShLangTessEvaluation; |
| 204 | } else if (suffix == QStringLiteral("geom" )) { |
| 205 | d->stage = EShLangGeometry; |
| 206 | } else if (suffix == QStringLiteral("comp" )) { |
| 207 | d->stage = EShLangCompute; |
| 208 | } else { |
| 209 | qWarning(msg: "QSpirvCompiler: Unknown shader stage, defaulting to vertex" ); |
| 210 | d->stage = EShLangVertex; |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | static inline EShLanguage mapShaderStage(QShader::Stage stage) |
| 215 | { |
| 216 | switch (stage) { |
| 217 | case QShader::VertexStage: |
| 218 | return EShLangVertex; |
| 219 | case QShader::TessellationControlStage: |
| 220 | return EShLangTessControl; |
| 221 | case QShader::TessellationEvaluationStage: |
| 222 | return EShLangTessEvaluation; |
| 223 | case QShader::GeometryStage: |
| 224 | return EShLangGeometry; |
| 225 | case QShader::FragmentStage: |
| 226 | return EShLangFragment; |
| 227 | case QShader::ComputeStage: |
| 228 | return EShLangCompute; |
| 229 | default: |
| 230 | return EShLangVertex; |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | void QSpirvCompiler::setSourceFileName(const QString &fileName, QShader::Stage stage) |
| 235 | { |
| 236 | if (!d->readFile(fn: fileName)) |
| 237 | return; |
| 238 | |
| 239 | d->stage = mapShaderStage(stage); |
| 240 | } |
| 241 | |
| 242 | void QSpirvCompiler::setSourceDevice(QIODevice *device, QShader::Stage stage, const QString &fileName) |
| 243 | { |
| 244 | setSourceString(sourceString: device->readAll(), stage, fileName); |
| 245 | } |
| 246 | |
| 247 | void QSpirvCompiler::setSourceString(const QByteArray &sourceString, QShader::Stage stage, const QString &fileName) |
| 248 | { |
| 249 | d->sourceFileName = fileName; // for error messages, include handling, etc. |
| 250 | d->source = sourceString; |
| 251 | d->batchableSource.clear(); |
| 252 | d->stage = mapShaderStage(stage); |
| 253 | } |
| 254 | |
| 255 | void QSpirvCompiler::setFlags(Flags flags) |
| 256 | { |
| 257 | d->flags = flags; |
| 258 | } |
| 259 | |
| 260 | void QSpirvCompiler::setPreamble(const QByteArray &preamble) |
| 261 | { |
| 262 | d->preamble = preamble; |
| 263 | } |
| 264 | |
| 265 | void QSpirvCompiler::setSGBatchingVertexInputLocation(int location) |
| 266 | { |
| 267 | d->batchAttrLoc = location; |
| 268 | } |
| 269 | |
| 270 | QByteArray QSpirvCompiler::compileToSpirv() |
| 271 | { |
| 272 | #ifdef TOKENIZER_DEBUG |
| 273 | QShaderRewriter::debugTokenizer(d->source); |
| 274 | #endif |
| 275 | |
| 276 | if (d->stage == EShLangVertex && d->flags.testFlag(flag: RewriteToMakeBatchableForSG) && d->batchableSource.isEmpty()) |
| 277 | d->batchableSource = QShaderRewriter::addZAdjustment(input: d->source, vertexInputLocation: d->batchAttrLoc); |
| 278 | |
| 279 | return d->compile() ? d->spirv : QByteArray(); |
| 280 | } |
| 281 | |
| 282 | QString QSpirvCompiler::errorMessage() const |
| 283 | { |
| 284 | return d->log; |
| 285 | } |
| 286 | |
| 287 | QT_END_NAMESPACE |
| 288 | |