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 | |