| 1 | // Copyright (C) 2008-2012 NVIDIA Corporation. |
| 2 | // Copyright (C) 2020 The Qt Company Ltd. |
| 3 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 4 | |
| 5 | #include <QtQuick3DRuntimeRender/private/qssgrhieffectsystem_p.h> |
| 6 | #include <QtQuick3DRuntimeRender/private/qssgrenderer_p.h> |
| 7 | #include <QtQuick3DRuntimeRender/private/qssgrhiquadrenderer_p.h> |
| 8 | #include "qssgrendercontextcore.h" |
| 9 | #include "qssgrendershadercodegenerator_p.h" |
| 10 | #include <qtquick3d_tracepoints_p.h> |
| 11 | |
| 12 | #include <QtQuick3DUtils/private/qssgassert_p.h> |
| 13 | |
| 14 | #include <QtCore/qloggingcategory.h> |
| 15 | |
| 16 | QT_BEGIN_NAMESPACE |
| 17 | |
| 18 | Q_DECLARE_LOGGING_CATEGORY(lcEffectSystem); |
| 19 | Q_LOGGING_CATEGORY(lcEffectSystem, "qt.quick3d.effects" ); |
| 20 | |
| 21 | struct QSSGRhiEffectTexture |
| 22 | { |
| 23 | QRhiTexture *texture = nullptr; |
| 24 | QRhiRenderPassDescriptor *renderPassDescriptor = nullptr; |
| 25 | QRhiTextureRenderTarget *renderTarget = nullptr; |
| 26 | QByteArray name; |
| 27 | |
| 28 | QSSGRhiSamplerDescription desc; |
| 29 | QSSGAllocateBufferFlags flags; |
| 30 | |
| 31 | ~QSSGRhiEffectTexture() |
| 32 | { |
| 33 | delete texture; |
| 34 | delete renderPassDescriptor; |
| 35 | delete renderTarget; |
| 36 | } |
| 37 | QSSGRhiEffectTexture &operator=(const QSSGRhiEffectTexture &) = delete; |
| 38 | }; |
| 39 | |
| 40 | QSSGRhiEffectSystem::QSSGRhiEffectSystem(const std::shared_ptr<QSSGRenderContextInterface> &sgContext) |
| 41 | : m_sgContext(sgContext) |
| 42 | { |
| 43 | } |
| 44 | |
| 45 | QSSGRhiEffectSystem::~QSSGRhiEffectSystem() |
| 46 | { |
| 47 | releaseResources(); |
| 48 | } |
| 49 | |
| 50 | void QSSGRhiEffectSystem::setup(QSize outputSize) |
| 51 | { |
| 52 | if (outputSize.isEmpty()) { |
| 53 | releaseResources(); |
| 54 | return; |
| 55 | } |
| 56 | m_outSize = outputSize; |
| 57 | } |
| 58 | |
| 59 | QSSGRhiEffectTexture *QSSGRhiEffectSystem::findTexture(const QByteArray &bufferName) |
| 60 | { |
| 61 | auto findTexture = [bufferName](const QSSGRhiEffectTexture *rt){ return rt->name == bufferName; }; |
| 62 | const auto foundIt = std::find_if(first: m_textures.cbegin(), last: m_textures.cend(), pred: findTexture); |
| 63 | QSSGRhiEffectTexture *result = foundIt == m_textures.cend() ? nullptr : *foundIt; |
| 64 | return result; |
| 65 | } |
| 66 | |
| 67 | QSSGRhiEffectTexture *QSSGRhiEffectSystem::getTexture(const QByteArray &bufferName, |
| 68 | const QSize &size, |
| 69 | QRhiTexture::Format format, |
| 70 | bool isFinalOutput, |
| 71 | const QSSGRenderEffect *inEffect, |
| 72 | quint8 viewCount) |
| 73 | { |
| 74 | QSSGRhiEffectTexture *result = findTexture(bufferName); |
| 75 | const bool gotMatch = result != nullptr; |
| 76 | |
| 77 | // If not found, look for an unused texture |
| 78 | if (!result) { |
| 79 | // ### This could be enhanced to try to find a texture with the right |
| 80 | // size/format/flags first. It is not essential because the texture will be |
| 81 | // recreated below if the size or format does not match, but it would |
| 82 | // be more optimal (for Effects with Buffers with sizeMultipliers on |
| 83 | // them, or ones that use different formats) to look for a matching |
| 84 | // size/format too instead of picking the first unused texture. |
| 85 | auto findUnused = [](const QSSGRhiEffectTexture *rt){ return rt->name.isEmpty(); }; |
| 86 | const auto found = std::find_if(first: m_textures.cbegin(), last: m_textures.cend(), pred: findUnused); |
| 87 | if (found != m_textures.cend()) { |
| 88 | result = *found; |
| 89 | result->desc = {}; |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | if (!result) { |
| 94 | result = new QSSGRhiEffectTexture {}; |
| 95 | m_textures.append(t: result); |
| 96 | } |
| 97 | |
| 98 | const auto &rhiCtx = m_sgContext->rhiContext(); |
| 99 | QRhi *rhi = rhiCtx->rhi(); |
| 100 | const bool formatChanged = result->texture && result->texture->format() != format; |
| 101 | const bool needsRebuild = result->texture && (result->texture->pixelSize() != size || formatChanged); |
| 102 | |
| 103 | QRhiTexture::Flags flags = QRhiTexture::RenderTarget; |
| 104 | if (isFinalOutput) // play nice with progressive/temporal AA |
| 105 | flags |= QRhiTexture::UsedAsTransferSource; |
| 106 | |
| 107 | if (!result->texture) { |
| 108 | if (viewCount >= 2) |
| 109 | result->texture = rhi->newTextureArray(format, arraySize: viewCount, pixelSize: size, sampleCount: 1, flags); |
| 110 | else |
| 111 | result->texture = rhi->newTexture(format, pixelSize: size, sampleCount: 1, flags); |
| 112 | result->texture->create(); |
| 113 | } else if (needsRebuild) { |
| 114 | result->texture->setFlags(flags); |
| 115 | result->texture->setPixelSize(size); |
| 116 | result->texture->setFormat(format); |
| 117 | result->texture->create(); |
| 118 | } |
| 119 | |
| 120 | if (!result->renderTarget) { |
| 121 | QRhiColorAttachment colorAttachment(result->texture); |
| 122 | colorAttachment.setMultiViewCount(viewCount); |
| 123 | QRhiTextureRenderTargetDescription desc(colorAttachment); |
| 124 | result->renderTarget = rhi->newTextureRenderTarget(desc); |
| 125 | result->renderPassDescriptor = result->renderTarget->newCompatibleRenderPassDescriptor(); |
| 126 | result->renderTarget->setRenderPassDescriptor(result->renderPassDescriptor); |
| 127 | result->renderTarget->create(); |
| 128 | m_pendingClears.insert(value: result->renderTarget); |
| 129 | } else if (needsRebuild) { |
| 130 | if (formatChanged) { |
| 131 | delete result->renderPassDescriptor; |
| 132 | result->renderPassDescriptor = result->renderTarget->newCompatibleRenderPassDescriptor(); |
| 133 | result->renderTarget->setRenderPassDescriptor(result->renderPassDescriptor); |
| 134 | } |
| 135 | result->renderTarget->create(); |
| 136 | m_pendingClears.insert(value: result->renderTarget); |
| 137 | } |
| 138 | |
| 139 | if (!gotMatch) { |
| 140 | QByteArray rtName = inEffect->debugObjectName.toLatin1(); |
| 141 | rtName += QByteArrayLiteral(" effect pass " ); |
| 142 | rtName += bufferName; |
| 143 | result->renderTarget->setName(rtName); |
| 144 | } |
| 145 | |
| 146 | result->name = bufferName; |
| 147 | return result; |
| 148 | } |
| 149 | |
| 150 | void QSSGRhiEffectSystem::releaseTexture(QSSGRhiEffectTexture *texture) |
| 151 | { |
| 152 | // Mark as unused by setting the name to empty, unless the Buffer had scene |
| 153 | // lifetime on it (then it needs to live on for ever). |
| 154 | if (!texture->flags.isSceneLifetime()) |
| 155 | texture->name = {}; |
| 156 | } |
| 157 | |
| 158 | void QSSGRhiEffectSystem::releaseTextures() |
| 159 | { |
| 160 | for (auto *t : std::as_const(t&: m_textures)) |
| 161 | releaseTexture(texture: t); |
| 162 | } |
| 163 | |
| 164 | QRhiTexture *QSSGRhiEffectSystem::process(const QSSGRenderLayer &layer, |
| 165 | QRhiTexture *inTexture, |
| 166 | QRhiTexture *inDepthTexture, |
| 167 | QVector2D cameraClipRange) |
| 168 | { |
| 169 | QSSG_ASSERT(m_sgContext != nullptr, return inTexture); |
| 170 | QSSG_ASSERT(layer.firstEffect != nullptr, return inTexture); |
| 171 | const auto &rhiContext = m_sgContext->rhiContext(); |
| 172 | const auto &renderer = m_sgContext->renderer(); |
| 173 | QSSG_ASSERT(rhiContext && renderer, return inTexture); |
| 174 | |
| 175 | m_depthTexture = inDepthTexture; |
| 176 | m_cameraClipRange = cameraClipRange; |
| 177 | |
| 178 | const auto viewCount = layer.viewCount; |
| 179 | |
| 180 | m_currentUbufIndex = 0; |
| 181 | // FIXME: Keeping the change minimal for now, but we should avoid the need for this cast. |
| 182 | QSSGRenderEffect *currentEffect = const_cast<QSSGRenderEffect *>(layer.firstEffect); |
| 183 | QSSGRhiEffectTexture firstTex{ .texture: inTexture, .renderPassDescriptor: nullptr, .renderTarget: nullptr, .name: {}, .desc: {}, .flags: {} }; |
| 184 | auto *latestOutput = doRenderEffect(inEffect: currentEffect, inTexture: &firstTex, viewCount); |
| 185 | firstTex.texture = nullptr; // make sure we don't delete inTexture when we go out of scope |
| 186 | |
| 187 | while ((currentEffect = currentEffect->m_nextEffect)) { |
| 188 | QSSGRhiEffectTexture *effectOut = doRenderEffect(inEffect: currentEffect, inTexture: latestOutput, viewCount); |
| 189 | releaseTexture(texture: latestOutput); |
| 190 | latestOutput = effectOut; |
| 191 | } |
| 192 | |
| 193 | releaseTextures(); |
| 194 | return latestOutput ? latestOutput->texture : nullptr; |
| 195 | } |
| 196 | |
| 197 | void QSSGRhiEffectSystem::releaseResources() |
| 198 | { |
| 199 | qDeleteAll(c: m_textures); |
| 200 | m_textures.clear(); |
| 201 | |
| 202 | m_shaderPipelines.clear(); |
| 203 | } |
| 204 | |
| 205 | QSSGRenderTextureFormat::Format QSSGRhiEffectSystem::overriddenOutputFormat(const QSSGRenderEffect *inEffect) |
| 206 | { |
| 207 | QSSGRenderTextureFormat::Format format = QSSGRenderTextureFormat::Unknown; |
| 208 | for (const QSSGRenderEffect::Command &c : inEffect->commands) { |
| 209 | QSSGCommand *cmd = c.command; |
| 210 | if (cmd->m_type == CommandType::BindTarget) { |
| 211 | QSSGBindTarget *targetCmd = static_cast<QSSGBindTarget *>(cmd); |
| 212 | format = targetCmd->m_outputFormat == QSSGRenderTextureFormat::Unknown |
| 213 | ? inEffect->outputFormat : targetCmd->m_outputFormat.format; |
| 214 | } |
| 215 | } |
| 216 | return format; |
| 217 | } |
| 218 | |
| 219 | QSSGRhiEffectTexture *QSSGRhiEffectSystem::doRenderEffect(const QSSGRenderEffect *inEffect, |
| 220 | QSSGRhiEffectTexture *inTexture, |
| 221 | quint8 viewCount) |
| 222 | { |
| 223 | // Run through the effect commands and render the effect. |
| 224 | qCDebug(lcEffectSystem) << "START effect " << inEffect->className; |
| 225 | QSSGRhiEffectTexture *finalOutputTexture = nullptr; |
| 226 | QSSGRhiEffectTexture *currentOutput = nullptr; |
| 227 | QSSGRhiEffectTexture *currentInput = inTexture; |
| 228 | for (const QSSGRenderEffect::Command &c : inEffect->commands) { |
| 229 | QSSGCommand *theCommand = c.command; |
| 230 | qCDebug(lcEffectSystem).noquote() << " >" << theCommand->typeAsString() << "--" << theCommand->debugString(); |
| 231 | |
| 232 | switch (theCommand->m_type) { |
| 233 | case CommandType::AllocateBuffer: |
| 234 | allocateBufferCmd(inCmd: static_cast<QSSGAllocateBuffer *>(theCommand), inTexture, inEffect, viewCount); |
| 235 | break; |
| 236 | |
| 237 | case CommandType::ApplyBufferValue: { |
| 238 | auto *applyCommand = static_cast<QSSGApplyBufferValue *>(theCommand); |
| 239 | |
| 240 | /* |
| 241 | BufferInput { buffer: buf } |
| 242 | -> INPUT (qt_inputTexture) in the shader samples the texture for Buffer buf in the pass |
| 243 | BufferInput { sampler: "ttt" } |
| 244 | -> ttt in the shader samples the input texture for the pass |
| 245 | (ttt also needs to be a TextureInput with a Texture{} to get the sampler declared in the shader code, |
| 246 | beware that without the BufferInput the behavior would change: ttt would then sample a dummy texture) |
| 247 | BufferInput { buffer: buf; sampler: "ttt" } |
| 248 | -> ttt in the shader samples the texture for Buffer buf in the pass |
| 249 | */ |
| 250 | |
| 251 | auto *buffer = applyCommand->m_bufferName.isEmpty() ? inTexture : findTexture(bufferName: applyCommand->m_bufferName); |
| 252 | if (applyCommand->m_samplerName.isEmpty()) |
| 253 | currentInput = buffer; |
| 254 | else |
| 255 | addTextureToShaderPipeline(name: applyCommand->m_samplerName, texture: buffer->texture, samplerDesc: buffer->desc); |
| 256 | break; |
| 257 | } |
| 258 | |
| 259 | case CommandType::ApplyInstanceValue: |
| 260 | applyInstanceValueCmd(inCmd: static_cast<QSSGApplyInstanceValue *>(theCommand), inEffect); |
| 261 | break; |
| 262 | |
| 263 | case CommandType::ApplyValue: |
| 264 | applyValueCmd(inCmd: static_cast<QSSGApplyValue *>(theCommand), inEffect); |
| 265 | break; |
| 266 | |
| 267 | case CommandType::BindBuffer: { |
| 268 | auto *bindCmd = static_cast<QSSGBindBuffer *>(theCommand); |
| 269 | currentOutput = findTexture(bufferName: bindCmd->m_bufferName); |
| 270 | break; |
| 271 | } |
| 272 | |
| 273 | case CommandType::BindShader: |
| 274 | bindShaderCmd(inCmd: static_cast<QSSGBindShader *>(theCommand), inEffect, viewCount); |
| 275 | break; |
| 276 | |
| 277 | case CommandType::BindTarget: { |
| 278 | auto targetCmd = static_cast<QSSGBindTarget*>(theCommand); |
| 279 | // matches overriddenOutputFormat() |
| 280 | QSSGRenderTextureFormat::Format f = targetCmd->m_outputFormat == QSSGRenderTextureFormat::Unknown ? |
| 281 | inEffect->outputFormat : targetCmd->m_outputFormat.format; |
| 282 | // f is now either Unknown (common case), or if the effect overrides the output format, then that |
| 283 | QRhiTexture::Format rhiFormat = f == QSSGRenderTextureFormat::Unknown ? |
| 284 | currentInput->texture->format() : QSSGBufferManager::toRhiFormat(format: f); |
| 285 | qCDebug(lcEffectSystem) << " Target format override" << QSSGBaseTypeHelpers::toString(value: f) << "Effective RHI format" << rhiFormat; |
| 286 | // Make sure we use different names for each effect inside one frame |
| 287 | QByteArray tmpName = QByteArrayLiteral("__output_" ).append(a: QByteArray::number(m_currentUbufIndex)); |
| 288 | currentOutput = getTexture(bufferName: tmpName, size: m_outSize, format: rhiFormat, isFinalOutput: true, inEffect, viewCount); |
| 289 | finalOutputTexture = currentOutput; |
| 290 | break; |
| 291 | } |
| 292 | |
| 293 | case CommandType::Render: |
| 294 | renderCmd(inTexture: currentInput, target: currentOutput, viewCount); |
| 295 | currentInput = inTexture; // default input for each new pass is defined to be original input |
| 296 | break; |
| 297 | |
| 298 | default: |
| 299 | qWarning() << "Effect command" << theCommand->typeAsString() << "not implemented" ; |
| 300 | break; |
| 301 | } |
| 302 | } |
| 303 | // TODO: release textures used by this effect now, instead of after processing all the effects |
| 304 | qCDebug(lcEffectSystem) << "END effect " << inEffect->className; |
| 305 | return finalOutputTexture; |
| 306 | } |
| 307 | |
| 308 | void QSSGRhiEffectSystem::allocateBufferCmd(const QSSGAllocateBuffer *inCmd, |
| 309 | QSSGRhiEffectTexture *inTexture, |
| 310 | const QSSGRenderEffect *inEffect, |
| 311 | quint8 viewCount) |
| 312 | { |
| 313 | // Note: Allocate is used both to allocate new, and refer to buffer created earlier |
| 314 | QSize bufferSize(m_outSize * qreal(inCmd->m_sizeMultiplier)); |
| 315 | |
| 316 | QSSGRenderTextureFormat f = inCmd->m_format; |
| 317 | QRhiTexture::Format rhiFormat = (f == QSSGRenderTextureFormat::Unknown) ? inTexture->texture->format() |
| 318 | : QSSGBufferManager::toRhiFormat(format: f); |
| 319 | |
| 320 | QSSGRhiEffectTexture *buf = getTexture(bufferName: inCmd->m_name, size: bufferSize, format: rhiFormat, isFinalOutput: false, inEffect, viewCount); |
| 321 | auto filter = QSSGRhiHelpers::toRhi(op: inCmd->m_filterOp); |
| 322 | auto tiling = QSSGRhiHelpers::toRhi(tiling: inCmd->m_texCoordOp); |
| 323 | buf->desc = { .minFilter: filter, .magFilter: filter, .mipmap: QRhiSampler::None, .hTiling: tiling, .vTiling: tiling, .zTiling: QRhiSampler::Repeat }; |
| 324 | buf->flags = inCmd->m_bufferFlags; |
| 325 | } |
| 326 | |
| 327 | void QSSGRhiEffectSystem::applyInstanceValueCmd(const QSSGApplyInstanceValue *inCmd, const QSSGRenderEffect *inEffect) |
| 328 | { |
| 329 | if (!m_currentShaderPipeline) |
| 330 | return; |
| 331 | |
| 332 | const bool setAll = inCmd->m_propertyName.isEmpty(); |
| 333 | for (const QSSGRenderEffect::Property &property : std::as_const(t: inEffect->properties)) { |
| 334 | if (setAll || property.name == inCmd->m_propertyName) { |
| 335 | m_currentShaderPipeline->setUniformValue(ubufData: m_currentUBufData, name: property.name, value: property.value, type: property.shaderDataType); |
| 336 | //qCDebug(lcEffectSystem) << "setUniformValue" << property.name << toString(property.shaderDataType) << "to" << property.value; |
| 337 | } |
| 338 | } |
| 339 | for (const QSSGRenderEffect::TextureProperty &textureProperty : std::as_const(t: inEffect->textureProperties)) { |
| 340 | if (setAll || textureProperty.name == inCmd->m_propertyName) { |
| 341 | bool texAdded = false; |
| 342 | QSSGRenderImage *image = textureProperty.texImage; |
| 343 | if (image) { |
| 344 | const auto &theBufferManager(m_sgContext->bufferManager()); |
| 345 | const QSSGRenderImageTexture texture = theBufferManager->loadRenderImage(image); |
| 346 | if (texture.m_texture) { |
| 347 | const QSSGRhiSamplerDescription desc{ |
| 348 | .minFilter: QSSGRhiHelpers::toRhi(op: textureProperty.minFilterType), |
| 349 | .magFilter: QSSGRhiHelpers::toRhi(op: textureProperty.magFilterType), |
| 350 | .mipmap: textureProperty.mipFilterType != QSSGRenderTextureFilterOp::None ? QSSGRhiHelpers::toRhi(op: textureProperty.mipFilterType) : QRhiSampler::None, |
| 351 | .hTiling: QSSGRhiHelpers::toRhi(tiling: textureProperty.horizontalClampType), |
| 352 | .vTiling: QSSGRhiHelpers::toRhi(tiling: textureProperty.verticalClampType), |
| 353 | .zTiling: QSSGRhiHelpers::toRhi(tiling: textureProperty.zClampType) |
| 354 | }; |
| 355 | addTextureToShaderPipeline(name: textureProperty.name, texture: texture.m_texture, samplerDesc: desc); |
| 356 | texAdded = true; |
| 357 | } |
| 358 | } |
| 359 | if (!texAdded) { |
| 360 | // Something went wrong, e.g. image file not found. Still need to add a dummy texture for the shader |
| 361 | qCDebug(lcEffectSystem) << "Using dummy texture for property" << textureProperty.name; |
| 362 | addTextureToShaderPipeline(name: textureProperty.name, texture: nullptr, samplerDesc: {}); |
| 363 | } |
| 364 | } |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | void QSSGRhiEffectSystem::applyValueCmd(const QSSGApplyValue *inCmd, const QSSGRenderEffect *inEffect) |
| 369 | { |
| 370 | if (!m_currentShaderPipeline) |
| 371 | return; |
| 372 | |
| 373 | const auto &properties = inEffect->properties; |
| 374 | const auto foundIt = std::find_if(first: properties.cbegin(), last: properties.cend(), pred: [inCmd](const QSSGRenderEffect::Property &prop) { |
| 375 | return (prop.name == inCmd->m_propertyName); |
| 376 | }); |
| 377 | |
| 378 | if (foundIt != properties.cend()) |
| 379 | m_currentShaderPipeline->setUniformValue(ubufData: m_currentUBufData, name: inCmd->m_propertyName, value: inCmd->m_value, type: foundIt->shaderDataType); |
| 380 | else |
| 381 | qWarning() << "Could not find effect property" << inCmd->m_propertyName; |
| 382 | } |
| 383 | |
| 384 | static const char *effect_builtin_textureMapUV = |
| 385 | "vec2 qt_effectTextureMapUV(vec2 uv)\n" |
| 386 | "{\n" |
| 387 | " return uv;\n" |
| 388 | "}\n" ; |
| 389 | |
| 390 | static const char *effect_builtin_textureMapUVFlipped = |
| 391 | "vec2 qt_effectTextureMapUV(vec2 uv)\n" |
| 392 | "{\n" |
| 393 | " return vec2(uv.x, 1.0 - uv.y);\n" |
| 394 | "}\n" ; |
| 395 | |
| 396 | QSSGRhiShaderPipelinePtr QSSGRhiEffectSystem::buildShaderForEffect(const QSSGBindShader &inCmd, |
| 397 | QSSGProgramGenerator &generator, |
| 398 | QSSGShaderLibraryManager &shaderLib, |
| 399 | QSSGShaderCache &shaderCache, |
| 400 | bool isYUpInFramebuffer, |
| 401 | int viewCount) |
| 402 | { |
| 403 | const auto &key = inCmd.m_shaderPathKey; |
| 404 | qCDebug(lcEffectSystem) << " generating new shader pipeline for: " << key; |
| 405 | |
| 406 | generator.beginProgram(); |
| 407 | |
| 408 | { |
| 409 | const QByteArray src = shaderLib.getShaderSource(inShaderPathKey: inCmd.m_shaderPathKey, type: QSSGShaderCache::ShaderType::Vertex); |
| 410 | QSSGStageGeneratorBase *vStage = generator.getStage(inStage: QSSGShaderGeneratorStage::Vertex); |
| 411 | // The variation based on isYUpInFramebuffer is captured in 'key' as |
| 412 | // well, so it is safe to vary the source code here. |
| 413 | vStage->append(data: isYUpInFramebuffer ? effect_builtin_textureMapUV : effect_builtin_textureMapUVFlipped); |
| 414 | vStage->append(data: src); |
| 415 | } |
| 416 | { |
| 417 | const QByteArray src = shaderLib.getShaderSource(inShaderPathKey: inCmd.m_shaderPathKey, type: QSSGShaderCache::ShaderType::Fragment); |
| 418 | QSSGStageGeneratorBase *fStage = generator.getStage(inStage: QSSGShaderGeneratorStage::Fragment); |
| 419 | fStage->append(data: src); |
| 420 | } |
| 421 | |
| 422 | return generator.compileGeneratedRhiShader(inMaterialInfoString: key, |
| 423 | inFeatureSet: shaderLib.getShaderMetaData(inShaderPathKey: inCmd.m_shaderPathKey, type: QSSGShaderCache::ShaderType::Fragment).features, |
| 424 | shaderLibraryManager&: shaderLib, |
| 425 | theCache&: shaderCache, |
| 426 | stageFlags: QSSGRhiShaderPipeline::UsedWithoutIa, |
| 427 | viewCount, |
| 428 | perTargetCompilation: false); |
| 429 | } |
| 430 | |
| 431 | void QSSGRhiEffectSystem::bindShaderCmd(const QSSGBindShader *inCmd, const QSSGRenderEffect *inEffect, quint8 viewCount) |
| 432 | { |
| 433 | QElapsedTimer timer; |
| 434 | timer.start(); |
| 435 | |
| 436 | m_currentTextures.clear(); |
| 437 | m_pendingClears.clear(); |
| 438 | m_currentShaderPipeline = nullptr; |
| 439 | |
| 440 | const auto &rhiCtx = m_sgContext->rhiContext(); |
| 441 | QRhi *rhi = rhiCtx->rhi(); |
| 442 | const auto &shaderLib = m_sgContext->shaderLibraryManager(); |
| 443 | const auto &shaderCache = m_sgContext->shaderCache(); |
| 444 | |
| 445 | // Now we need a proper unique key (unique in the scene), the filenames are |
| 446 | // not sufficient. This means that using the same shader source files in |
| 447 | // multiple Effects in the same scene will work. It wouldn't if all those |
| 448 | // Effects reused the same QSSGRhiShaderPipeline (i.e. if the only cache |
| 449 | // key was the m_shaderPathKey). |
| 450 | QSSGEffectSceneCacheKey cacheKey; |
| 451 | cacheKey.m_shaderPathKey = inCmd->m_shaderPathKey; |
| 452 | cacheKey.m_cmd = quintptr(inCmd); |
| 453 | cacheKey.m_ubufIndex = m_currentUbufIndex; |
| 454 | cacheKey.updateHashCode(); |
| 455 | |
| 456 | // look for a runtime pipeline |
| 457 | const auto it = m_shaderPipelines.constFind(key: cacheKey); |
| 458 | if (it != m_shaderPipelines.cend()) |
| 459 | m_currentShaderPipeline = (*it).get(); |
| 460 | |
| 461 | QByteArray qsbcKey; |
| 462 | QSSGShaderFeatures features; |
| 463 | if (!m_currentShaderPipeline) { // don't spend time if already got the pipeline |
| 464 | features = shaderLib->getShaderMetaData(inShaderPathKey: inCmd->m_shaderPathKey, type: QSSGShaderCache::ShaderType::Fragment).features; |
| 465 | qsbcKey = QQsbCollection::EntryDesc::generateSha(materialKey: inCmd->m_shaderPathKey, featureSet: QQsbCollection::toFeatureSet(ssgFeatureSet: features)); |
| 466 | } |
| 467 | |
| 468 | // Check if there's a build-time generated entry for this effect |
| 469 | if (!m_currentShaderPipeline && !shaderLib->m_preGeneratedShaderEntries.isEmpty()) { |
| 470 | const QQsbCollection::EntryMap &pregenEntries = shaderLib->m_preGeneratedShaderEntries; |
| 471 | const auto foundIt = pregenEntries.constFind(value: QQsbCollection::Entry(qsbcKey)); |
| 472 | if (foundIt != pregenEntries.cend()) { |
| 473 | // The result here is always a new QSSGRhiShaderPipeline, which |
| 474 | // fulfills the requirements of our local cache (cmd/ubufIndex in |
| 475 | // cacheKey, not needed here since the result is a new object). |
| 476 | const auto &shader = shaderCache->newPipelineFromPregenerated(inKey: inCmd->m_shaderPathKey, |
| 477 | inFeatures: features, |
| 478 | entry: *foundIt, |
| 479 | obj: *inEffect, |
| 480 | stageFlags: QSSGRhiShaderPipeline::UsedWithoutIa); |
| 481 | m_shaderPipelines.insert(key: cacheKey, value: shader); |
| 482 | m_currentShaderPipeline = shader.get(); |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | if (!m_currentShaderPipeline) { |
| 487 | // Try the persistent (disk-based) cache then. The result here is |
| 488 | // always a new QSSGRhiShaderPipeline, which fulfills the requirements |
| 489 | // of our local cache (cmd/ubufIndex in cacheKey, not needed here since |
| 490 | // the result is a new object). Alternatively, the result may be null |
| 491 | // if there was no hit. |
| 492 | const auto &shaderPipeline = shaderCache->tryNewPipelineFromPersistentCache(qsbcKey, |
| 493 | inKey: inCmd->m_shaderPathKey, |
| 494 | inFeatures: features, |
| 495 | stageFlags: QSSGRhiShaderPipeline::UsedWithoutIa); |
| 496 | if (shaderPipeline) { |
| 497 | m_shaderPipelines.insert(key: cacheKey, value: shaderPipeline); |
| 498 | m_currentShaderPipeline = shaderPipeline.get(); |
| 499 | } |
| 500 | } |
| 501 | |
| 502 | if (!m_currentShaderPipeline) { |
| 503 | // Final option, generate the shader pipeline |
| 504 | Q_TRACE_SCOPE(QSSG_generateShader); |
| 505 | Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DGenerateShader); |
| 506 | const auto &generator = m_sgContext->shaderProgramGenerator(); |
| 507 | if (auto stages = buildShaderForEffect(inCmd: *inCmd, generator&: *generator, shaderLib&: *shaderLib, shaderCache&: *shaderCache, isYUpInFramebuffer: rhi->isYUpInFramebuffer(), viewCount)) { |
| 508 | m_shaderPipelines.insert(key: cacheKey, value: stages); |
| 509 | m_currentShaderPipeline = stages.get(); |
| 510 | } |
| 511 | Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DGenerateShader, 0, inEffect->profilingId); |
| 512 | } |
| 513 | |
| 514 | const auto &rhiContext = m_sgContext->rhiContext(); |
| 515 | |
| 516 | if (m_currentShaderPipeline) { |
| 517 | const void *cacheKey1 = reinterpret_cast<const void *>(this); |
| 518 | const void *cacheKey2 = reinterpret_cast<const void *>(qintptr(m_currentUbufIndex)); |
| 519 | QSSGRhiDrawCallData &dcd = QSSGRhiContextPrivate::get(q: rhiContext.get())->drawCallData(key: { .cid: cacheKey1, .model: cacheKey2, .entry: nullptr, .entryIdx: 0 }); |
| 520 | m_currentShaderPipeline->ensureCombinedUniformBuffer(ubuf: &dcd.ubuf); |
| 521 | m_currentUBufData = dcd.ubuf->beginFullDynamicBufferUpdateForCurrentFrame(); |
| 522 | } else { |
| 523 | m_currentUBufData = nullptr; |
| 524 | } |
| 525 | |
| 526 | QSSGRhiContextStats::get(rhiCtx&: *rhiContext).registerEffectShaderGenerationTime(ms: timer.elapsed()); |
| 527 | } |
| 528 | |
| 529 | void QSSGRhiEffectSystem::renderCmd(QSSGRhiEffectTexture *inTexture, QSSGRhiEffectTexture *target, quint8 viewCount) |
| 530 | { |
| 531 | if (!m_currentShaderPipeline) |
| 532 | return; |
| 533 | |
| 534 | if (!target) { |
| 535 | qWarning(msg: "No effect render target?" ); |
| 536 | return; |
| 537 | } |
| 538 | |
| 539 | // the shader only uses one of these (or none) |
| 540 | addTextureToShaderPipeline(QByteArrayLiteral("qt_inputTexture" ), texture: inTexture->texture, samplerDesc: inTexture->desc); |
| 541 | addTextureToShaderPipeline(QByteArrayLiteral("qt_inputTextureArray" ), texture: inTexture->texture, samplerDesc: inTexture->desc); |
| 542 | |
| 543 | const auto &rhiContext = m_sgContext->rhiContext(); |
| 544 | const auto &renderer = m_sgContext->renderer(); |
| 545 | |
| 546 | QRhiCommandBuffer *cb = rhiContext->commandBuffer(); |
| 547 | cb->debugMarkBegin(QByteArrayLiteral("Post-processing effect" )); |
| 548 | Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderPass); |
| 549 | |
| 550 | for (QRhiTextureRenderTarget *rt : m_pendingClears) { |
| 551 | // Effects like motion blur use an accumulator texture that should |
| 552 | // start out empty (and they are sampled in the first pass), so such |
| 553 | // textures need an explicit clear. It is not applicable for the common |
| 554 | // case of outputting into a texture because that will get a clear |
| 555 | // anyway when rendering the quad. |
| 556 | if (rt != target->renderTarget) { |
| 557 | cb->beginPass(rt, colorClearValue: Qt::transparent, depthStencilClearValue: { 1.0f, 0 }, resourceUpdates: nullptr, flags: rhiContext->commonPassFlags()); |
| 558 | QSSGRHICTX_STAT(rhiContext, beginRenderPass(rt)); |
| 559 | cb->endPass(); |
| 560 | QSSGRHICTX_STAT(rhiContext, endRenderPass()); |
| 561 | } |
| 562 | } |
| 563 | m_pendingClears.clear(); |
| 564 | |
| 565 | const QSize inputSize = inTexture->texture->pixelSize(); |
| 566 | const QSize outputSize = target->texture->pixelSize(); |
| 567 | addCommonEffectUniforms(inputSize, outputSize); |
| 568 | |
| 569 | QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(q: rhiContext.get()); |
| 570 | |
| 571 | const void *cacheKey1 = reinterpret_cast<const void *>(this); |
| 572 | const void *cacheKey2 = reinterpret_cast<const void *>(qintptr(m_currentUbufIndex)); |
| 573 | QSSGRhiDrawCallData &dcd = rhiCtxD->drawCallData(key: { .cid: cacheKey1, .model: cacheKey2, .entry: nullptr, .entryIdx: 0 }); |
| 574 | dcd.ubuf->endFullDynamicBufferUpdateForCurrentFrame(); |
| 575 | m_currentUBufData = nullptr; |
| 576 | |
| 577 | QRhiResourceUpdateBatch *rub = rhiContext->rhi()->nextResourceUpdateBatch(); |
| 578 | renderer->rhiQuadRenderer()->prepareQuad(rhiCtx: rhiContext.get(), maybeRub: rub); |
| 579 | |
| 580 | // do resource bindings |
| 581 | const QRhiShaderResourceBinding::StageFlags VISIBILITY_ALL = |
| 582 | QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage; |
| 583 | QSSGRhiShaderResourceBindingList bindings; |
| 584 | for (const QSSGRhiTexture &rhiTex : m_currentTextures) { |
| 585 | int binding = m_currentShaderPipeline->bindingForTexture(name: rhiTex.name); |
| 586 | if (binding < 0) // may not be used in the shader (think qt_inputTexture, it's not given a shader samples INPUT) |
| 587 | continue; |
| 588 | qCDebug(lcEffectSystem) << " -> texture binding" << binding << "for" << rhiTex.name; |
| 589 | // Make sure to bind all samplers even if the texture is missing, otherwise we can get crash on some graphics APIs |
| 590 | QRhiTexture *texture = rhiTex.texture ? rhiTex.texture : rhiContext->dummyTexture(flags: {}, rub); |
| 591 | bindings.addTexture(binding, |
| 592 | stage: QRhiShaderResourceBinding::FragmentStage, |
| 593 | tex: texture, |
| 594 | sampler: rhiContext->sampler(samplerDescription: rhiTex.samplerDesc)); |
| 595 | } |
| 596 | bindings.addUniformBuffer(binding: 0, stage: VISIBILITY_ALL, buf: dcd.ubuf); |
| 597 | |
| 598 | QRhiShaderResourceBindings *srb = rhiCtxD->srb(bindings); |
| 599 | |
| 600 | QSSGRhiGraphicsPipelineState ps; |
| 601 | ps.viewport = QRhiViewport(0, 0, float(outputSize.width()), float(outputSize.height())); |
| 602 | ps.samples = target->renderTarget->sampleCount(); |
| 603 | ps.viewCount = viewCount; |
| 604 | QSSGRhiGraphicsPipelineStatePrivate::setShaderPipeline(ps, pipeline: m_currentShaderPipeline); |
| 605 | |
| 606 | renderer->rhiQuadRenderer()->recordRenderQuadPass(rhiCtx: rhiContext.get(), ps: &ps, srb, rt: target->renderTarget, flags: QSSGRhiQuadRenderer::UvCoords); |
| 607 | m_currentUbufIndex++; |
| 608 | cb->debugMarkEnd(); |
| 609 | Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderPass, 0, QByteArrayLiteral("post_processing_effect" )); |
| 610 | } |
| 611 | |
| 612 | void QSSGRhiEffectSystem::addCommonEffectUniforms(const QSize &inputSize, const QSize &outputSize) |
| 613 | { |
| 614 | const auto &rhiContext = m_sgContext->rhiContext(); |
| 615 | QRhi *rhi = rhiContext->rhi(); |
| 616 | |
| 617 | QMatrix4x4 mvp; |
| 618 | if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC()) |
| 619 | mvp.data()[5] = -1.0f; |
| 620 | m_currentShaderPipeline->setUniformValue(ubufData: m_currentUBufData, name: "qt_modelViewProjection" , value: mvp, type: QSSGRenderShaderValue::Matrix4x4); |
| 621 | |
| 622 | QVector2D size(inputSize.width(), inputSize.height()); |
| 623 | m_currentShaderPipeline->setUniformValue(ubufData: m_currentUBufData, name: "qt_inputSize" , value: size, type: QSSGRenderShaderValue::Vec2); |
| 624 | |
| 625 | size = QVector2D(outputSize.width(), outputSize.height()); |
| 626 | m_currentShaderPipeline->setUniformValue(ubufData: m_currentUBufData, name: "qt_outputSize" , value: size, type: QSSGRenderShaderValue::Vec2); |
| 627 | |
| 628 | float fc = float(m_sgContext->renderer()->frameCount()); |
| 629 | m_currentShaderPipeline->setUniformValue(ubufData: m_currentUBufData, name: "qt_frame_num" , value: fc, type: QSSGRenderShaderValue::Float); |
| 630 | |
| 631 | // Bames and values for uniforms that are also used by default and/or |
| 632 | // custom materials must always match, effects must not deviate. |
| 633 | |
| 634 | m_currentShaderPipeline->setUniformValue(ubufData: m_currentUBufData, name: "qt_cameraProperties" , value: m_cameraClipRange, type: QSSGRenderShaderValue::Vec2); |
| 635 | |
| 636 | float vp = rhi->isYUpInFramebuffer() ? 1.0f : -1.0f; |
| 637 | m_currentShaderPipeline->setUniformValue(ubufData: m_currentUBufData, name: "qt_normalAdjustViewportFactor" , value: vp, type: QSSGRenderShaderValue::Float); |
| 638 | |
| 639 | const float nearClip = rhi->isClipDepthZeroToOne() ? 0.0f : -1.0f; |
| 640 | m_currentShaderPipeline->setUniformValue(ubufData: m_currentUBufData, name: "qt_nearClipValue" , value: nearClip, type: QSSGRenderShaderValue::Float); |
| 641 | |
| 642 | if (m_depthTexture) { |
| 643 | static const QSSGRhiSamplerDescription depthSamplerDesc { |
| 644 | .minFilter: QRhiSampler::Nearest, .magFilter: QRhiSampler::Nearest, |
| 645 | .mipmap: QRhiSampler::None, |
| 646 | .hTiling: QRhiSampler::ClampToEdge, .vTiling: QRhiSampler::ClampToEdge, .zTiling: QRhiSampler::Repeat |
| 647 | }; |
| 648 | addTextureToShaderPipeline(name: "qt_depthTexture" , texture: m_depthTexture, samplerDesc: depthSamplerDesc); |
| 649 | addTextureToShaderPipeline(name: "qt_depthTextureArray" , texture: m_depthTexture, samplerDesc: depthSamplerDesc); |
| 650 | } |
| 651 | } |
| 652 | |
| 653 | void QSSGRhiEffectSystem::addTextureToShaderPipeline(const QByteArray &name, |
| 654 | QRhiTexture *texture, |
| 655 | const QSSGRhiSamplerDescription &samplerDescription) |
| 656 | { |
| 657 | if (!m_currentShaderPipeline) |
| 658 | return; |
| 659 | |
| 660 | static const QSSGRhiSamplerDescription defaultDescription { .minFilter: QRhiSampler::Linear, .magFilter: QRhiSampler::Linear, .mipmap: QRhiSampler::None, |
| 661 | .hTiling: QRhiSampler::ClampToEdge, .vTiling: QRhiSampler::ClampToEdge, .zTiling: QRhiSampler::Repeat }; |
| 662 | bool validDescription = samplerDescription.magFilter != QRhiSampler::None; |
| 663 | |
| 664 | // This is a map for a reason: there can be multiple calls to this function |
| 665 | // for the same 'name', with a different 'texture', take the last value |
| 666 | // into account only. |
| 667 | m_currentTextures.insert(key: name, value: { .name: name, .texture: texture, .samplerDesc: validDescription ? samplerDescription : defaultDescription}); |
| 668 | } |
| 669 | |
| 670 | QT_END_NAMESPACE |
| 671 | |