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 <SPIRV/GlslangToSpv.h> |
13 | QT_WARNING_POP |
14 | |
15 | //#define TOKENIZER_DEBUG |
16 | |
17 | QT_BEGIN_NAMESPACE |
18 | |
19 | const TBuiltInResource resourceLimits = { |
20 | /* .MaxLights = */ .maxLights: 32, |
21 | /* .MaxClipPlanes = */ .maxClipPlanes: 6, |
22 | /* .MaxTextureUnits = */ .maxTextureUnits: 32, |
23 | /* .MaxTextureCoords = */ .maxTextureCoords: 32, |
24 | /* .MaxVertexAttribs = */ .maxVertexAttribs: 64, |
25 | /* .MaxVertexUniformComponents = */ .maxVertexUniformComponents: 4096, |
26 | /* .MaxVaryingFloats = */ .maxVaryingFloats: 64, |
27 | /* .MaxVertexTextureImageUnits = */ .maxVertexTextureImageUnits: 32, |
28 | /* .MaxCombinedTextureImageUnits = */ .maxCombinedTextureImageUnits: 80, |
29 | /* .MaxTextureImageUnits = */ .maxTextureImageUnits: 32, |
30 | /* .MaxFragmentUniformComponents = */ .maxFragmentUniformComponents: 4096, |
31 | /* .MaxDrawBuffers = */ .maxDrawBuffers: 32, |
32 | /* .MaxVertexUniformVectors = */ .maxVertexUniformVectors: 128, |
33 | /* .MaxVaryingVectors = */ .maxVaryingVectors: 8, |
34 | /* .MaxFragmentUniformVectors = */ .maxFragmentUniformVectors: 16, |
35 | /* .MaxVertexOutputVectors = */ .maxVertexOutputVectors: 16, |
36 | /* .MaxFragmentInputVectors = */ .maxFragmentInputVectors: 15, |
37 | /* .MinProgramTexelOffset = */ .minProgramTexelOffset: -8, |
38 | /* .MaxProgramTexelOffset = */ .maxProgramTexelOffset: 7, |
39 | /* .MaxClipDistances = */ .maxClipDistances: 8, |
40 | /* .MaxComputeWorkGroupCountX = */ .maxComputeWorkGroupCountX: 65535, |
41 | /* .MaxComputeWorkGroupCountY = */ .maxComputeWorkGroupCountY: 65535, |
42 | /* .MaxComputeWorkGroupCountZ = */ .maxComputeWorkGroupCountZ: 65535, |
43 | /* .MaxComputeWorkGroupSizeX = */ .maxComputeWorkGroupSizeX: 1024, |
44 | /* .MaxComputeWorkGroupSizeY = */ .maxComputeWorkGroupSizeY: 1024, |
45 | /* .MaxComputeWorkGroupSizeZ = */ .maxComputeWorkGroupSizeZ: 64, |
46 | /* .MaxComputeUniformComponents = */ .maxComputeUniformComponents: 1024, |
47 | /* .MaxComputeTextureImageUnits = */ .maxComputeTextureImageUnits: 16, |
48 | /* .MaxComputeImageUniforms = */ .maxComputeImageUniforms: 8, |
49 | /* .MaxComputeAtomicCounters = */ .maxComputeAtomicCounters: 8, |
50 | /* .MaxComputeAtomicCounterBuffers = */ .maxComputeAtomicCounterBuffers: 1, |
51 | /* .MaxVaryingComponents = */ .maxVaryingComponents: 60, |
52 | /* .MaxVertexOutputComponents = */ .maxVertexOutputComponents: 64, |
53 | /* .MaxGeometryInputComponents = */ .maxGeometryInputComponents: 64, |
54 | /* .MaxGeometryOutputComponents = */ .maxGeometryOutputComponents: 128, |
55 | /* .MaxFragmentInputComponents = */ .maxFragmentInputComponents: 128, |
56 | /* .MaxImageUnits = */ .maxImageUnits: 8, |
57 | /* .MaxCombinedImageUnitsAndFragmentOutputs = */ .maxCombinedImageUnitsAndFragmentOutputs: 8, |
58 | /* .MaxCombinedShaderOutputResources = */ .maxCombinedShaderOutputResources: 8, |
59 | /* .MaxImageSamples = */ .maxImageSamples: 0, |
60 | /* .MaxVertexImageUniforms = */ .maxVertexImageUniforms: 0, |
61 | /* .MaxTessControlImageUniforms = */ .maxTessControlImageUniforms: 0, |
62 | /* .MaxTessEvaluationImageUniforms = */ .maxTessEvaluationImageUniforms: 0, |
63 | /* .MaxGeometryImageUniforms = */ .maxGeometryImageUniforms: 0, |
64 | /* .MaxFragmentImageUniforms = */ .maxFragmentImageUniforms: 8, |
65 | /* .MaxCombinedImageUniforms = */ .maxCombinedImageUniforms: 8, |
66 | /* .MaxGeometryTextureImageUnits = */ .maxGeometryTextureImageUnits: 16, |
67 | /* .MaxGeometryOutputVertices = */ .maxGeometryOutputVertices: 256, |
68 | /* .MaxGeometryTotalOutputComponents = */ .maxGeometryTotalOutputComponents: 1024, |
69 | /* .MaxGeometryUniformComponents = */ .maxGeometryUniformComponents: 1024, |
70 | /* .MaxGeometryVaryingComponents = */ .maxGeometryVaryingComponents: 64, |
71 | /* .MaxTessControlInputComponents = */ .maxTessControlInputComponents: 128, |
72 | /* .MaxTessControlOutputComponents = */ .maxTessControlOutputComponents: 128, |
73 | /* .MaxTessControlTextureImageUnits = */ .maxTessControlTextureImageUnits: 16, |
74 | /* .MaxTessControlUniformComponents = */ .maxTessControlUniformComponents: 1024, |
75 | /* .MaxTessControlTotalOutputComponents = */ .maxTessControlTotalOutputComponents: 4096, |
76 | /* .MaxTessEvaluationInputComponents = */ .maxTessEvaluationInputComponents: 128, |
77 | /* .MaxTessEvaluationOutputComponents = */ .maxTessEvaluationOutputComponents: 128, |
78 | /* .MaxTessEvaluationTextureImageUnits = */ .maxTessEvaluationTextureImageUnits: 16, |
79 | /* .MaxTessEvaluationUniformComponents = */ .maxTessEvaluationUniformComponents: 1024, |
80 | /* .MaxTessPatchComponents = */ .maxTessPatchComponents: 120, |
81 | /* .MaxPatchVertices = */ .maxPatchVertices: 32, |
82 | /* .MaxTessGenLevel = */ .maxTessGenLevel: 64, |
83 | /* .MaxViewports = */ .maxViewports: 16, |
84 | /* .MaxVertexAtomicCounters = */ .maxVertexAtomicCounters: 0, |
85 | /* .MaxTessControlAtomicCounters = */ .maxTessControlAtomicCounters: 0, |
86 | /* .MaxTessEvaluationAtomicCounters = */ .maxTessEvaluationAtomicCounters: 0, |
87 | /* .MaxGeometryAtomicCounters = */ .maxGeometryAtomicCounters: 0, |
88 | /* .MaxFragmentAtomicCounters = */ .maxFragmentAtomicCounters: 8, |
89 | /* .MaxCombinedAtomicCounters = */ .maxCombinedAtomicCounters: 8, |
90 | /* .MaxAtomicCounterBindings = */ .maxAtomicCounterBindings: 1, |
91 | /* .MaxVertexAtomicCounterBuffers = */ .maxVertexAtomicCounterBuffers: 0, |
92 | /* .MaxTessControlAtomicCounterBuffers = */ .maxTessControlAtomicCounterBuffers: 0, |
93 | /* .MaxTessEvaluationAtomicCounterBuffers = */ .maxTessEvaluationAtomicCounterBuffers: 0, |
94 | /* .MaxGeometryAtomicCounterBuffers = */ .maxGeometryAtomicCounterBuffers: 0, |
95 | /* .MaxFragmentAtomicCounterBuffers = */ .maxFragmentAtomicCounterBuffers: 1, |
96 | /* .MaxCombinedAtomicCounterBuffers = */ .maxCombinedAtomicCounterBuffers: 1, |
97 | /* .MaxAtomicCounterBufferSize = */ .maxAtomicCounterBufferSize: 16384, |
98 | /* .MaxTransformFeedbackBuffers = */ .maxTransformFeedbackBuffers: 4, |
99 | /* .MaxTransformFeedbackInterleavedComponents = */ .maxTransformFeedbackInterleavedComponents: 64, |
100 | /* .MaxCullDistances = */ .maxCullDistances: 8, |
101 | /* .MaxCombinedClipAndCullDistances = */ .maxCombinedClipAndCullDistances: 8, |
102 | /* .MaxSamples = */ .maxSamples: 4, |
103 | /* .maxMeshOutputVerticesNV = */ 256, |
104 | /* .maxMeshOutputPrimitivesNV = */ 512, |
105 | /* .maxMeshWorkGroupSizeX_NV = */ 32, |
106 | /* .maxMeshWorkGroupSizeY_NV = */ 1, |
107 | /* .maxMeshWorkGroupSizeZ_NV = */ 1, |
108 | /* .maxTaskWorkGroupSizeX_NV = */ 32, |
109 | /* .maxTaskWorkGroupSizeY_NV = */ 1, |
110 | /* .maxTaskWorkGroupSizeZ_NV = */ 1, |
111 | /* .maxMeshViewCountNV = */ 4, |
112 | /* .maxDualSourceDrawBuffersEXT = */ 1, |
113 | |
114 | /* .limits = */ { |
115 | /* .nonInductiveForLoops = */ 1, |
116 | /* .whileLoops = */ 1, |
117 | /* .doWhileLoops = */ 1, |
118 | /* .generalUniformIndexing = */ 1, |
119 | /* .generalAttributeMatrixVectorIndexing = */ 1, |
120 | /* .generalVaryingIndexing = */ 1, |
121 | /* .generalSamplerIndexing = */ 1, |
122 | /* .generalVariableIndexing = */ 1, |
123 | /* .generalConstantMatrixVectorIndexing = */ 1, |
124 | } |
125 | }; |
126 | |
127 | struct QSpirvCompilerPrivate |
128 | { |
129 | bool readFile(const QString &fn); |
130 | bool compile(); |
131 | |
132 | QString sourceFileName; |
133 | QByteArray source; |
134 | QByteArray batchableSource; |
135 | EShLanguage stage = EShLangVertex; |
136 | QSpirvCompiler::Flags flags; |
137 | QByteArray preamble; |
138 | int batchAttrLoc = 7; |
139 | QByteArray spirv; |
140 | QString log; |
141 | }; |
142 | |
143 | bool QSpirvCompilerPrivate::readFile(const QString &fn) |
144 | { |
145 | QFile f(fn); |
146 | if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { |
147 | qWarning(msg: "QSpirvCompiler: Failed to open %s" , qPrintable(fn)); |
148 | return false; |
149 | } |
150 | source = f.readAll(); |
151 | batchableSource.clear(); |
152 | sourceFileName = fn; |
153 | f.close(); |
154 | return true; |
155 | } |
156 | |
157 | using namespace QtShaderTools; |
158 | |
159 | class Includer : public glslang::TShader::Includer |
160 | { |
161 | public: |
162 | IncludeResult *includeLocal(const char *, |
163 | const char *includerName, |
164 | size_t inclusionDepth) override |
165 | { |
166 | Q_UNUSED(inclusionDepth); |
167 | return readFile(headerName, includerName); |
168 | } |
169 | |
170 | IncludeResult *includeSystem(const char *, |
171 | const char *includerName, |
172 | size_t inclusionDepth) override |
173 | { |
174 | Q_UNUSED(inclusionDepth); |
175 | return readFile(headerName, includerName); |
176 | } |
177 | |
178 | void releaseInclude(IncludeResult *result) override |
179 | { |
180 | if (result) { |
181 | delete static_cast<QByteArray *>(result->userData); |
182 | delete result; |
183 | } |
184 | } |
185 | |
186 | private: |
187 | IncludeResult *readFile(const char *, const char *includerName); |
188 | }; |
189 | |
190 | glslang::TShader::Includer::IncludeResult *Includer::readFile(const char *, const char *includerName) |
191 | { |
192 | // Just treat the included name as relative to the includer: |
193 | // Take the path from the includer, append the included name, remove redundancies. |
194 | // This should work also for qrc (source filenames with qrc:/ or :/ prefix). |
195 | |
196 | QString includer = QString::fromUtf8(utf8: includerName); |
197 | if (includer.isEmpty()) |
198 | includer = QLatin1String("." ); |
199 | QString included = QFileInfo(includer).canonicalPath() + QLatin1Char('/') + QString::fromUtf8(utf8: headerName); |
200 | included = QFileInfo(included).canonicalFilePath(); |
201 | if (included.isEmpty()) { |
202 | qWarning(msg: "QSpirvCompiler: Failed to find include file %s" , headerName); |
203 | return nullptr; |
204 | } |
205 | QFile f(included); |
206 | if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { |
207 | qWarning(msg: "QSpirvCompiler: Failed to read include file %s" , qPrintable(included)); |
208 | return nullptr; |
209 | } |
210 | |
211 | QByteArray *data = new QByteArray; |
212 | *data = f.readAll(); |
213 | return new IncludeResult(included.toStdString(), data->constData(), data->size(), data); |
214 | } |
215 | |
216 | class GlobalInit |
217 | { |
218 | public: |
219 | GlobalInit() { glslang::InitializeProcess(); } |
220 | ~GlobalInit() { glslang::FinalizeProcess(); } |
221 | }; |
222 | |
223 | bool QSpirvCompilerPrivate::compile() |
224 | { |
225 | log.clear(); |
226 | |
227 | const bool useBatchable = (stage == EShLangVertex && flags.testFlag(flag: QSpirvCompiler::RewriteToMakeBatchableForSG)); |
228 | const QByteArray *actualSource = useBatchable ? &batchableSource : &source; |
229 | if (actualSource->isEmpty()) |
230 | return false; |
231 | |
232 | static GlobalInit globalInit; |
233 | |
234 | glslang::TShader shader(stage); |
235 | const QByteArray fn = sourceFileName.toUtf8(); |
236 | const char *fnStr = fn.constData(); |
237 | const char *srcStr = actualSource->constData(); |
238 | const int size = actualSource->size(); |
239 | shader.setStringsWithLengthsAndNames(s: &srcStr, l: &size, names: &fnStr, n: 1); |
240 | if (!preamble.isEmpty()) { |
241 | // Line numbers in errors and #version are not affected by having a |
242 | // preamble, which is just what we need. |
243 | shader.setPreamble(preamble.constData()); |
244 | } |
245 | |
246 | shader.setEnvInput(lang: glslang::EShSourceGlsl, envStage: stage, client: glslang::EShClientVulkan, version: 100); |
247 | shader.setEnvClient(client: glslang::EShClientVulkan, version: glslang::EShTargetVulkan_1_0); |
248 | shader.setEnvTarget(lang: glslang::EshTargetSpv, version: glslang::EShTargetSpv_1_0); |
249 | |
250 | int messages = EShMsgDefault; |
251 | if (flags.testFlag(flag: QSpirvCompiler::FullDebugInfo)) // embed source |
252 | messages |= EShMsgDebugInfo; |
253 | |
254 | Includer includer; |
255 | if (!shader.parse(builtInResources: &resourceLimits, defaultVersion: 100, forwardCompatible: false, messages: EShMessages(messages), includer)) { |
256 | qWarning(msg: "QSpirvCompiler: Failed to parse shader" ); |
257 | log = QString::fromUtf8(utf8: shader.getInfoLog()).trimmed(); |
258 | return false; |
259 | } |
260 | |
261 | glslang::TProgram program; |
262 | program.addShader(shader: &shader); |
263 | if (!program.link(EShMsgDefault)) { |
264 | qWarning(msg: "QSpirvCompiler: Link failed" ); |
265 | log = QString::fromUtf8(utf8: shader.getInfoLog()).trimmed(); |
266 | return false; |
267 | } |
268 | |
269 | // The only interesting option here is the debug info, optimizations and |
270 | // such do not happen at this level. |
271 | glslang::SpvOptions options; |
272 | options.generateDebugInfo = flags.testFlag(flag: QSpirvCompiler::FullDebugInfo); |
273 | |
274 | std::vector<unsigned int> spv; |
275 | glslang::GlslangToSpv(intermediate: *program.getIntermediate(stage), spirv&: spv, options: &options); |
276 | if (!spv.size()) { |
277 | qWarning(msg: "Failed to generate SPIR-V" ); |
278 | return false; |
279 | } |
280 | |
281 | spirv.resize(size: int(spv.size() * 4)); |
282 | memcpy(dest: spirv.data(), src: spv.data(), n: spirv.size()); |
283 | |
284 | return true; |
285 | } |
286 | |
287 | QSpirvCompiler::QSpirvCompiler() |
288 | : d(new QSpirvCompilerPrivate) |
289 | { |
290 | } |
291 | |
292 | QSpirvCompiler::~QSpirvCompiler() |
293 | { |
294 | delete d; |
295 | } |
296 | |
297 | void QSpirvCompiler::setSourceFileName(const QString &fileName) |
298 | { |
299 | if (!d->readFile(fn: fileName)) |
300 | return; |
301 | |
302 | const QString suffix = QFileInfo(fileName).suffix(); |
303 | if (suffix == QStringLiteral("vert" )) { |
304 | d->stage = EShLangVertex; |
305 | } else if (suffix == QStringLiteral("frag" )) { |
306 | d->stage = EShLangFragment; |
307 | } else if (suffix == QStringLiteral("tesc" )) { |
308 | d->stage = EShLangTessControl; |
309 | } else if (suffix == QStringLiteral("tese" )) { |
310 | d->stage = EShLangTessEvaluation; |
311 | } else if (suffix == QStringLiteral("geom" )) { |
312 | d->stage = EShLangGeometry; |
313 | } else if (suffix == QStringLiteral("comp" )) { |
314 | d->stage = EShLangCompute; |
315 | } else { |
316 | qWarning(msg: "QSpirvCompiler: Unknown shader stage, defaulting to vertex" ); |
317 | d->stage = EShLangVertex; |
318 | } |
319 | } |
320 | |
321 | static inline EShLanguage mapShaderStage(QShader::Stage stage) |
322 | { |
323 | switch (stage) { |
324 | case QShader::VertexStage: |
325 | return EShLangVertex; |
326 | case QShader::TessellationControlStage: |
327 | return EShLangTessControl; |
328 | case QShader::TessellationEvaluationStage: |
329 | return EShLangTessEvaluation; |
330 | case QShader::GeometryStage: |
331 | return EShLangGeometry; |
332 | case QShader::FragmentStage: |
333 | return EShLangFragment; |
334 | case QShader::ComputeStage: |
335 | return EShLangCompute; |
336 | default: |
337 | return EShLangVertex; |
338 | } |
339 | } |
340 | |
341 | void QSpirvCompiler::setSourceFileName(const QString &fileName, QShader::Stage stage) |
342 | { |
343 | if (!d->readFile(fn: fileName)) |
344 | return; |
345 | |
346 | d->stage = mapShaderStage(stage); |
347 | } |
348 | |
349 | void QSpirvCompiler::setSourceDevice(QIODevice *device, QShader::Stage stage, const QString &fileName) |
350 | { |
351 | setSourceString(sourceString: device->readAll(), stage, fileName); |
352 | } |
353 | |
354 | void QSpirvCompiler::setSourceString(const QByteArray &sourceString, QShader::Stage stage, const QString &fileName) |
355 | { |
356 | d->sourceFileName = fileName; // for error messages, include handling, etc. |
357 | d->source = sourceString; |
358 | d->batchableSource.clear(); |
359 | d->stage = mapShaderStage(stage); |
360 | } |
361 | |
362 | void QSpirvCompiler::setFlags(Flags flags) |
363 | { |
364 | d->flags = flags; |
365 | } |
366 | |
367 | void QSpirvCompiler::setPreamble(const QByteArray &preamble) |
368 | { |
369 | d->preamble = preamble; |
370 | } |
371 | |
372 | void QSpirvCompiler::setSGBatchingVertexInputLocation(int location) |
373 | { |
374 | d->batchAttrLoc = location; |
375 | } |
376 | |
377 | QByteArray QSpirvCompiler::compileToSpirv() |
378 | { |
379 | #ifdef TOKENIZER_DEBUG |
380 | QShaderRewriter::debugTokenizer(d->source); |
381 | #endif |
382 | |
383 | if (d->stage == EShLangVertex && d->flags.testFlag(flag: RewriteToMakeBatchableForSG) && d->batchableSource.isEmpty()) |
384 | d->batchableSource = QShaderRewriter::addZAdjustment(input: d->source, vertexInputLocation: d->batchAttrLoc); |
385 | |
386 | return d->compile() ? d->spirv : QByteArray(); |
387 | } |
388 | |
389 | QString QSpirvCompiler::errorMessage() const |
390 | { |
391 | return d->log; |
392 | } |
393 | |
394 | QT_END_NAMESPACE |
395 | |