// Copyright (C) 2020 Klaralvdalens Datakonsult AB (KDAB).
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "pipelineuboset_p.h"
#include <rendercommand_p.h>
#include <renderview_p.h>
#include <rhibuffer_p.h>
#include <shaderparameterpack_p.h>
#include <shadervariables_p.h>
#include <rhigraphicspipeline_p.h>
#include <rhiresourcemanagers_p.h>
#include <submissioncontext_p.h>
#include <rhi/qrhi.h>
#include <Qt3DRender/private/nodemanagers_p.h>
#include <Qt3DRender/private/buffermanager_p.h>
#include <Qt3DRender/private/stringtoint_p.h>
#include <Qt3DRender/private/buffer_p.h>
#include <Qt3DCore/private/vector_helper_p.h>

QT_BEGIN_NAMESPACE

namespace Qt3DRender {

namespace Render {

namespace Rhi {

namespace {
// This is the minimum max UBO size requirements with GL
// therefore safe to assume that is also the baseline for other graphics APIs
constexpr size_t MaxUBOByteSize = 16384;
}

PipelineUBOSet::PipelineUBOSet()
{
}

PipelineUBOSet::~PipelineUBOSet()
{
    // Resource release is performed by RHIGraphicsPipeline::cleanup()
}

void PipelineUBOSet::clear()
{
    m_renderCommands.clear();
}

void PipelineUBOSet::addRenderCommand(const RenderCommand &cmd)
{
    m_renderCommands.push_back(&cmd);
}

void PipelineUBOSet::setResourceManager(RHIResourceManagers *manager)
{
    m_resourceManagers = manager;
}

void PipelineUBOSet::setNodeManagers(NodeManagers *managers)
{
    m_nodeManagers = managers;
}

void PipelineUBOSet::initializeLayout(SubmissionContext *ctx, RHIShader *shader)
{
    // We should only be called with a clean Pipeline
    Q_ASSERT(m_rvUBO.buffer.isNull());

    m_rvUBO.binding = 0;
    m_rvUBO.blockSize = sizeof(RenderViewUBO);

    m_commandsUBO.binding = 1;
    m_commandsUBO.blockSize = sizeof(CommandUBO);
    m_commandsUBO.alignedBlockSize = ctx->rhi()->ubufAligned((m_commandsUBO.blockSize));
    m_commandsUBO.alignment = size_t(ctx->rhi()->ubufAlignment());
    m_commandsUBO.commandsPerUBO = MaxUBOByteSize / m_commandsUBO.alignedBlockSize;

    // For UBO, we try to create a single large UBO that will contain frontend
    // Qt3D UBO data at various offsets
    const std::vector<ShaderUniformBlock> &uniformBlocks = shader->uniformBlocks();
    for (const ShaderUniformBlock &block : uniformBlocks) {
        if (block.m_binding > 1) { // Binding 0 and 1 are for RV and Command UBOs
            const size_t alignedBlockSize = size_t(ctx->rhi()->ubufAligned(block.m_size));
            m_materialsUBOs.push_back(
                        { block.m_binding,
                          block.m_size,
                          alignedBlockSize,
                          size_t(ctx->rhi()->ubufAlignment()),
                          MaxUBOByteSize / alignedBlockSize,
                          {} });
        }
    }

    // For SSBO, we have one SSBO per frontend Qt3D SSBO
    m_storageBlocks =  shader->storageBlocks();
}

void PipelineUBOSet::releaseResources()
{
    Q_ASSERT(m_resourceManagers);
    RHIBufferManager *bufferManager = m_resourceManagers->rhiBufferManager();

    bufferManager->release(m_rvUBO.buffer);

    for (const HRHIBuffer &hBuf : m_commandsUBO.buffers)
        bufferManager->release(hBuf);

    m_rvUBO = {};
    m_commandsUBO = {};

    if (!m_materialsUBOs.empty()) {
        for (const MultiUBOBufferWithBindingAndBlockSize &ubo : m_materialsUBOs) {
            for (const HRHIBuffer &hBuf : ubo.buffers)
                bufferManager->release(hBuf);
        }
        m_materialsUBOs.clear();
    }
}

bool PipelineUBOSet::allocateUBOs(SubmissionContext *ctx)
{
    RHIBufferManager *bufferManager = m_resourceManagers->rhiBufferManager();
    // Note: RHIBuffer only releases/recreates when it needs more size than what it
    // can currently hold
    Q_ASSERT(m_resourceManagers);
    const bool dynamic = true;
    const size_t commandCount = std::max(m_renderCommands.size(), size_t(1));

    if (m_rvUBO.buffer.isNull())
        m_rvUBO.buffer = bufferManager->allocateResource();

    // RHIBuffer only reallocates if size is < than required
    m_rvUBO.buffer->allocate(QByteArray(m_rvUBO.blockSize, '\0'), dynamic);
    // Binding buffer ensure underlying RHI resource is created
    m_rvUBO.buffer->bind(ctx, RHIBuffer::UniformBuffer);

    auto allocateMultiUBOsForCommands = [&] (MultiUBOBufferWithBindingAndBlockSize &ubo) {
        // Round up
        const size_t uboCount = std::ceil(float(commandCount) / ubo.commandsPerUBO);

        if (ubo.buffers.size() < uboCount)
            ubo.buffers.resize(uboCount);

        for (HRHIBuffer &buf : ubo.buffers) {
            if (buf.isNull())
                buf = bufferManager->allocateResource();
            // We need to take into account any minimum alignment requirement for dynamic offsets
            buf->allocate(QByteArray(MaxUBOByteSize, '\0'), dynamic);
            buf->bind(ctx, RHIBuffer::UniformBuffer);
        }
    };

    // Commands UBOs
    allocateMultiUBOsForCommands(m_commandsUBO);

    // Material UBOs
    for (MultiUBOBufferWithBindingAndBlockSize &ubo : m_materialsUBOs) {
        if (ubo.binding > 1) // Binding 0 and 1 are for RV and Command UBOs
            allocateMultiUBOsForCommands(ubo);
    }

    // SSBO are using RHIBuffer directly, nothing we need to handle ourselves

    return true;
}

size_t PipelineUBOSet::distanceToCommand(const RenderCommand &cmd) const
{
    const auto it = std::find(m_renderCommands.begin(), m_renderCommands.end(),
                              &cmd);
    if (Q_UNLIKELY(it == m_renderCommands.end())) {
        qCWarning(Backend) << "Command not found in UBOSet";
        return 0;
    }
    return std::distance(m_renderCommands.begin(), it);
}

std::vector<QRhiCommandBuffer::DynamicOffset> PipelineUBOSet::offsets(const RenderCommand &cmd) const
{
    std::vector<QRhiCommandBuffer::DynamicOffset> offsets;
    offsets.reserve(1 + m_materialsUBOs.size());

    // RenderCommand offset
    // binding, offset
    const size_t dToCmd = distanceToCommand(cmd);
    {
        // Compute offset relative to the select UBO in the subset
        const auto localOffset = m_commandsUBO.localOffsetInBufferForCommand(dToCmd);
        offsets.push_back({1, quint32(localOffset)});
    }

    for (const MultiUBOBufferWithBindingAndBlockSize &materialUBO : m_materialsUBOs) {
        // Compute offset relative to the select UBO in the subset
        const size_t localOffset = materialUBO.localOffsetInBufferForCommand(dToCmd);
        offsets.push_back({materialUBO.binding, quint32(localOffset)});
    }

    return offsets;
}

// Note: this function is currently the same as resourceBindings However, once
// https://codereview.qt-project.org/c/qt/qtbase/+/307472 lands we can get rid
// of passing the RenderCommand here as well as having to provide the actual
// Resources in that function which means that one will be about providing the
// Pipeline resourceLayout only and resourceBindings will be about providing
// the actual resources for a RenderCommand
std::vector<QRhiShaderResourceBinding> PipelineUBOSet::resourceLayout(const RHIShader *shader)
{
    const QRhiShaderResourceBinding::StageFlags stages = QRhiShaderResourceBinding::VertexStage|QRhiShaderResourceBinding::FragmentStage;
    std::vector<QRhiShaderResourceBinding> bindings = {
        QRhiShaderResourceBinding::uniformBuffer(0, stages, nullptr),
        QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(1, stages, nullptr, sizeof(CommandUBO))
    };

    // TO DO: Handle Parameters that directly define a UBO or SSBO
    //    const auto &blockToUBOs = parameterPack.uniformBuffers();
    //    const auto &blockToSSBOs = parameterPack.shaderStorageBuffers();

    // Create additional empty UBO Buffer for UBO with binding point > 1 (since
    // we assume 0 and 1 and for Qt3D standard values)
    for (const MultiUBOBufferWithBindingAndBlockSize &ubo : m_materialsUBOs)
        bindings.push_back(
                    QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(
                        ubo.binding, stages, nullptr, ubo.blockSize));

    // Samplers
    for (const ShaderAttribute &samplerAttribute : shader->samplers()) {
        bindings.push_back(QRhiShaderResourceBinding::sampledTexture(
                               samplerAttribute.m_location,
                               stages, nullptr, nullptr));
    }

    // SSBO
    for (const ShaderStorageBlock &b : m_storageBlocks) {
        bindings.push_back(QRhiShaderResourceBinding::bufferLoadStore(b.m_binding,
                                                                      stages|QRhiShaderResourceBinding::ComputeStage,
                                                                      nullptr));
    }

    return bindings;
}

std::vector<QRhiShaderResourceBinding> PipelineUBOSet::resourceBindings(const RenderCommand &command)
{
    RHITextureManager *textureManager = m_resourceManagers->rhiTextureManager();
    RHIShader *shader = command.m_rhiShader;
    const QRhiShaderResourceBinding::StageFlags stages = QRhiShaderResourceBinding::ComputeStage|QRhiShaderResourceBinding::VertexStage|QRhiShaderResourceBinding::FragmentStage;
    std::vector<QRhiShaderResourceBinding> bindings = {
        QRhiShaderResourceBinding::uniformBuffer(0, stages, m_rvUBO.buffer->rhiBuffer()),
    };

    const size_t dToCmd = distanceToCommand(command);

    // CommandUBO
    {
        const HRHIBuffer &commandUBO = m_commandsUBO.bufferForCommand(dToCmd);
        bindings.push_back(
                    QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(1,
                                                                              stages,
                                                                              commandUBO->rhiBuffer(),
                                                                              int(m_commandsUBO.alignedBlockSize)));
    }

    // Create additional empty UBO Buffer for UBO with binding point > 1 (since
    // we assume 0 and 1 and for Qt3D standard values)
    // Note: if a Parameter provides a UBO Buffer, its content will have been
    // copied at right offset into the matching materials UBO
    for (const MultiUBOBufferWithBindingAndBlockSize &ubo : m_materialsUBOs) {
        const HRHIBuffer &materialUBO = ubo.bufferForCommand(dToCmd);
        bindings.push_back(
                    QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(
                        ubo.binding, stages, materialUBO->rhiBuffer(), int(ubo.alignedBlockSize)));
    }

    // Samplers
    const std::vector<ShaderAttribute> &shaderSamplers = shader->samplers();
    std::vector<int> samplersSetIds;
    for (const ShaderParameterPack::NamedResource &textureParameter : command.m_parameterPack.textures()) {
        const HRHITexture &handle = textureManager->getOrAcquireHandle(textureParameter.nodeId);
        const RHITexture *textureData = handle.data();

        for (const ShaderAttribute &samplerAttribute : shaderSamplers) {
            if (samplerAttribute.m_nameId == textureParameter.glslNameId) {
                const auto rhiTexture = textureData->getRhiTexture();
                const auto rhiSampler = textureData->getRhiSampler();
                if (rhiTexture && rhiSampler) {
                    bindings.push_back(QRhiShaderResourceBinding::sampledTexture(
                                           samplerAttribute.m_location,
                                           stages, rhiTexture, rhiSampler));
                    samplersSetIds.push_back(samplerAttribute.m_nameId);
                }
            }
        }
    }

    // Some backends (Vulkan) expect the number of bindings to be exactly the
    // same between the set used to create the pipeline in resourceLayout and
    // the set used to draw with said pipeline generated here.
    // This can be problematic if no parameter is provided for that resource
    const std::vector<ShaderAttribute> unsetSamplers = [&] () {
        std::vector<ShaderAttribute> unset;
        for (const ShaderAttribute &attr : shaderSamplers) {
            const bool matchingSetNameIdFound =
                    std::find_if(samplersSetIds.begin(),
                                 samplersSetIds.end(),
                                 [&attr] (int nameId) {
                return attr.m_nameId == nameId;
            }) != samplersSetIds.end();
            if (!matchingSetNameIdFound)
                unset.push_back(attr);
        }
        return unset;
    }();

    // Generate an empty binding for unset samplers
    const int envLightIrradianceNameId = StringToInt::lookupId(QStringLiteral("envLight_irradiance"));
    const int envLightSpecularNameId = StringToInt::lookupId(QStringLiteral("envLight_specular"));
    for (const ShaderAttribute &sampler : unsetSamplers) {
        if (sampler.m_nameId != envLightSpecularNameId && sampler.m_nameId != envLightIrradianceNameId)
            qCWarning(Backend) << "Sampler" << sampler.m_name << "wasn't set on material. Rendering might not work as expected";
    }

    // SSBO
    for (const BlockToSSBO &ssbo : command.m_parameterPack.shaderStorageBuffers()) {
        RHIBuffer *buffer = m_resourceManagers->rhiBufferManager()->lookupResource(ssbo.m_bufferID);
        if (buffer) {
            Q_ASSERT(stages & QRhiShaderResourceBinding::ComputeStage);
            bindings.push_back(QRhiShaderResourceBinding::bufferLoadStore(
                                   ssbo.m_bindingIndex,
                                   stages,
                                   buffer->rhiBuffer()));
        }
    }

    return bindings;
}

void PipelineUBOSet::uploadUBOs(SubmissionContext *ctx, RenderView *rv)
{
    // Update UBO data for RV and RC data
    m_rvUBO.buffer->update(QByteArray::fromRawData(reinterpret_cast<const char *>(rv->renderViewUBO()),
                                                   sizeof(RenderViewUBO)));
    int distance = 0;
    for (const RenderCommand *command : m_renderCommands) {
        uploadUBOsForCommand(*command, distance);
        ++distance;
    }

    // Trigger actual upload to GPU
    m_rvUBO.buffer->bind(ctx, RHIBuffer::UniformBuffer);
    for (const HRHIBuffer &ubo : m_commandsUBO.buffers)
        ubo->bind(ctx, RHIBuffer::UniformBuffer);
    for (const MultiUBOBufferWithBindingAndBlockSize &multiUbo : m_materialsUBOs) {
        for (const HRHIBuffer &ubo : multiUbo.buffers)
            ubo->bind(ctx, RHIBuffer::UniformBuffer);
    }
}

namespace {
/*
void printUpload(const UniformValue &value, const QShaderDescription::BlockVariable &member, int arrayOffset)
{
    switch (member.type) {
    case QShaderDescription::VariableType::Int:
        qDebug() << "Updating" << member.name << "with int data: " << *value.constData<int>()
                 << " (offset: " << member.offset + arrayOffset << ", size: " << member.size << ")";
        break;
    case QShaderDescription::VariableType::Float:
        qDebug() << "Updating" << member.name << "with float data: " << *value.constData<float>()
                 << " (offset: " << member.offset + arrayOffset << ", size: " << member.size << ")";
        break;
    case QShaderDescription::VariableType::Vec2:
        qDebug() << "Updating" << member.name << "with vec2 data: " << value.constData<float>()[0]
                 << ", " << value.constData<float>()[1] << " (offset: " << member.offset
                 << ", size: " << member.size + arrayOffset << ")";
        break;
    case QShaderDescription::VariableType::Vec3:
        qDebug() << "Updating" << member.name << "with vec3 data: " << value.constData<float>()[0]
                 << ", " << value.constData<float>()[1] << ", " << value.constData<float>()[2]
                 << " (offset: " << member.offset + arrayOffset << ", size: " << member.size << ")";
        break;
    case QShaderDescription::VariableType::Vec4:
        qDebug() << "Updating" << member.name << "with vec4 data: " << value.constData<float>()[0]
                 << ", " << value.constData<float>()[1] << ", " << value.constData<float>()[2]
                 << ", " << value.constData<float>()[3] << " (offset: " << member.offset + arrayOffset
                 << ", size: " << member.size << ")";
        break;
    default:
        qDebug() << "Updating" << member.name << "with data: " << value.constData<char>();
        break;
    }
}
//*/

inline void uploadDataToUBO(const QByteArray rawData,
                            const PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize *ubo,
                            const RHIShader::UBO_Member &member,
                            size_t distanceToCommand, int arrayOffset = 0)
{
    const HRHIBuffer &buffer = ubo->bufferForCommand(distanceToCommand);
    const size_t localOffset = ubo->localOffsetInBufferForCommand(distanceToCommand);
    buffer->update(rawData, int(localOffset) + member.blockVariable.offset + arrayOffset);
}

QByteArray rawDataForUniformValue(const QShaderDescription::BlockVariable &blockVariable,
                                  const UniformValue &value,
                                  bool requiresCopy)
{
    const QByteArray rawData = requiresCopy
              ? QByteArray(value.constData<char>(), std::min(value.byteSize(), blockVariable.size))
              : QByteArray::fromRawData(value.constData<char>(), std::min(value.byteSize(), blockVariable.size));
    const int matrixStride = blockVariable.matrixStride;
    int arrayStride = blockVariable.arrayStride;
    const int firstArrayDim = !blockVariable.arrayDims.empty() ? blockVariable.arrayDims.first() : 0;

    if (blockVariable.arrayDims.size() > 1)
        qCWarning(Backend) << "Multi Dimension arrays not handled yet";

    if (arrayStride != 0 && matrixStride != 0)
        qCWarning(Backend) << "Arrays of matrices not handled yet";

    // Special cases for arrays
    if (firstArrayDim > 0) {
        // It appears that for a float myArray[8]; stride is reported as being
        // 0 but expected size 128 bytes which means 16 bytes per float
        // In that case we compute the arrayStride ourselves
        if (arrayStride == 0)
            arrayStride = blockVariable.size / firstArrayDim;
        if (arrayStride != 0) {
            QByteArray newRawData(firstArrayDim * arrayStride, '\0');
            const int byteSizePerEntry = value.elementByteSize();
            for (int i = 0, m = std::min(firstArrayDim, value.elementCount()); i < m; ++i) {
                std::memcpy(newRawData.data() + i * arrayStride,
                            rawData.constData() + i * byteSizePerEntry,
                            byteSizePerEntry);
            }
            return newRawData;
        }
    }

    // Special cases for matrices which might have to be aligned to a vec4 stride
    if (matrixStride != 0 && value.byteSize() % matrixStride != 0) {
        // Find number of rows
        const int rows = blockVariable.size / matrixStride;
        QByteArray newRawData = QByteArray(rows * matrixStride, '\0');
        const int dataSizePerRow = value.byteSize() / rows;
        for (int i = 0; i < rows; ++i) {
            std::memcpy(newRawData.data() + i * matrixStride,
                        rawData.constData() + i * dataSizePerRow, dataSizePerRow);
        }
        return newRawData;
    }

    // Note: we currently don't handle array of matrices

    return rawData;
}

void uploadUniform(const PackUniformHash &uniforms,
                   const PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize *ubo,
                   const RHIShader::UBO_Member &member,
                   size_t distanceToCommand, int arrayOffset = 0)
{
    if (!uniforms.contains(member.nameId))
        return;

    const UniformValue &value = uniforms.value(member.nameId);
    // Textures / Images / Buffers can't be UBO members
    if (value.valueType() != UniformValue::ScalarValue)
        return;

    // We can avoid the copies since the raw data copes from the UniformValue
    // which remain alive until the frame has been submitted
    const bool requiresCopy = false;
    const QByteArray rawData = rawDataForUniformValue(member.blockVariable,
                                                      value,
                                                      requiresCopy);
    uploadDataToUBO(rawData, ubo, member, distanceToCommand, arrayOffset);

    // printUpload(value, member.blockVariable, arrayOffset);
}

} // anonymous

void PipelineUBOSet::uploadShaderDataProperty(const ShaderData *shaderData,
                              const PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize *ubo,
                              const RHIShader::UBO_Member &uboMemberInstance,
                              size_t distanceToCommand, int arrayOffset)
{
    const std::vector<RHIShader::UBO_Member> &structMembers = uboMemberInstance.structMembers;
    const QHash<QString, ShaderData::PropertyValue> &properties = shaderData->properties();
    const int structBaseOffset = uboMemberInstance.blockVariable.offset;
    for (const RHIShader::UBO_Member &member : structMembers) {
        // TO DO: Use nameIds instead
        const auto it = properties.find(member.blockVariable.name);
        if (it != properties.end()) {
            const ShaderData::PropertyValue &value = *it;
            if (value.isNode) {
                // Nested ShaderData
                const ShaderData *child = m_nodeManagers->shaderDataManager()->lookupResource(value.value.value<Qt3DCore::QNodeId>());
                if (child)
                    uploadShaderDataProperty(child, ubo, member,
                                             distanceToCommand,
                                             structBaseOffset + arrayOffset);
                continue;
            }
            if (value.isTransformed) {
                // TO DO: Handle this
                qWarning() << "ShaderData transformed properties not handled yet";
                // QVariant transformedValue = shaderData->getTransformedProperty(&value, viewMatrix);
            }

            // Value is a Scalar or a Scalar Array
            const UniformValue v = UniformValue::fromVariant(value.value);
            Q_ASSERT(v.valueType() == UniformValue::ScalarValue);

            // We have to make a copy here
            const bool requiresCopy = true;
            const QByteArray rawData = rawDataForUniformValue(member.blockVariable,
                                                              v,
                                                              requiresCopy);
            uploadDataToUBO(rawData, ubo, member, distanceToCommand, structBaseOffset + arrayOffset);

            // printUpload(v, member.blockVariable, structBaseOffset + arrayOffset);
        }
    }
}

void PipelineUBOSet::uploadUBOsForCommand(const RenderCommand &command,
                                          const size_t distanceToCommand)
{
    Q_ASSERT(m_nodeManagers);
    RHIShader *shader = command.m_rhiShader;
    if (!shader)
        return;

    {
        const HRHIBuffer &commandUBOBuffer = m_commandsUBO.bufferForCommand(distanceToCommand);
        const size_t localOffset = m_commandsUBO.localOffsetInBufferForCommand(distanceToCommand);
        commandUBOBuffer->update(QByteArray::fromRawData(
                                     reinterpret_cast<const char *>(&command.m_commandUBO),
                                     sizeof(CommandUBO)),
                                 int(localOffset));
    }

    const std::vector<RHIShader::UBO_Block> &uboBlocks = shader->uboBlocks();
    const ShaderParameterPack &parameterPack = command.m_parameterPack;
    const PackUniformHash &uniforms = parameterPack.uniforms();

    // Update Buffer CPU side data based on uniforms being set

    auto findMaterialUBOForBlock = [this] (const RHIShader::UBO_Block &uboBlock)
            -> PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize* {
        auto it = std::find_if(m_materialsUBOs.begin(), m_materialsUBOs.end(),
                               [&uboBlock] (const PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize &materialUBO) {
            return materialUBO.binding == uboBlock.block.m_binding;
        });

        if (it == m_materialsUBOs.end())
            return nullptr;
        return &*it;
    };

    auto findUboBlockForBinding = [&uboBlocks] (int blockBinding) -> const RHIShader::UBO_Block * {
        auto it = std::find_if(uboBlocks.begin(), uboBlocks.end(), [&blockBinding] (const RHIShader::UBO_Block &block) {
                return block.block.m_binding == blockBinding;
        });
        if (it == uboBlocks.end())
            return nullptr;
        return &*it;
    };

    // Scalar / Texture Parameter to UBO
    for (const RHIShader::UBO_Block &uboBlock : uboBlocks) {

        // No point in trying to update Bindings 0 or 1 which are reserved
        // for Qt3D default uniforms
        const ShaderUniformBlock &block = uboBlock.block;
        if (block.m_binding <= 1)
            continue;

        // Update UBO with uniform value
        const PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize *ubo = findMaterialUBOForBlock(uboBlock);
        if (ubo == nullptr)
            continue;

        for (const RHIShader::UBO_Member &member : std::as_const(uboBlock.members)) {
            const QShaderDescription::BlockVariable &blockVariable = member.blockVariable;

            // Array
            if (!blockVariable.arrayDims.empty()) {
                if (!blockVariable.structMembers.empty()) { // Array of structs
                    // we treat structMembers as arrayMembers when we are dealing with an array of structs´
                    const size_t arr0 = size_t(blockVariable.arrayDims[0]);
                    const size_t m = std::max(arr0, member.structMembers.size());
                    for (size_t i = 0; i < m; ++i) {
                        const RHIShader::UBO_Member &arrayMember = member.structMembers[i];
                        for (const RHIShader::UBO_Member &arrayStructMember : arrayMember.structMembers) {
                            uploadUniform(uniforms, ubo,
                                          arrayStructMember,
                                          distanceToCommand,
                                          int(i * blockVariable.size / arr0));
                        }
                    }
                } else { // Array of scalars
                    uploadUniform(uniforms, ubo,
                                  member, distanceToCommand);
                }
            } else {
                if (!blockVariable.structMembers.empty()) { // Struct
                    for (const RHIShader::UBO_Member &structMember : member.structMembers) {
                        uploadUniform(uniforms, ubo,
                                      structMember,
                                      distanceToCommand);
                    }
                } else { // Scalar
                    uploadUniform(uniforms, ubo,
                                  member, distanceToCommand);
                }
            }
        }
    }

    // User provided UBO
    // Since we create per Material UBO, if we directly provide a UBO as a parameter
    // we will have to copy content of the parameter's UBO into the Material UBO at the
    // property offset
    for (const BlockToUBO &ubo : parameterPack.uniformBuffers()) {
        // Find RHIShader::UBO_Block for UBO
        const RHIShader::UBO_Block *block = findUboBlockForBinding(ubo.m_bindingIndex);
        if (block == nullptr)
            continue;
        // Find Material UBO for Block
        PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize *materialsUBO = findMaterialUBOForBlock(*block);
        if (materialsUBO == nullptr)
            continue;

        // Copy content of UBO buffer to materialUBO at offset
        Buffer *uboBuffer = m_nodeManagers->bufferManager()->lookupResource(ubo.m_bufferID);
        if (!uboBuffer)
            continue;

        const HRHIBuffer &materialBuffer = materialsUBO->bufferForCommand(distanceToCommand);
        const size_t localOffsetIntoBuffer = materialsUBO->localOffsetInBufferForCommand(distanceToCommand);
        materialBuffer->update(uboBuffer->data(), int(localOffsetIntoBuffer));
    }

    // ShaderData -> convenience for filling a struct member of a UBO
    // Note: we could use setDefaultUniformBlockShaderDataValue to unpack all values as individual uniforms
    // but setting the whole ShaderData properties at once is a lot more efficient
    for (const ShaderDataForUBO &shaderDataForUbo : parameterPack.shaderDatasForUBOs()) {
        // Find ShaderData backend
        const ShaderData *shaderData = m_nodeManagers->shaderDataManager()->lookupResource(shaderDataForUbo.m_shaderDataID);
        if (shaderData == nullptr)
            continue;
        // Find RHIShader::UBO_Block for ShaderData
        const RHIShader::UBO_Block *block = findUboBlockForBinding(shaderDataForUbo.m_bindingIndex);
        if (block == nullptr)
            continue;
        // Find Material UBO for Block
        PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize *materialsUBO = findMaterialUBOForBlock(*block);
        if (materialsUBO == nullptr)
            continue;

        // Upload ShaderData property that match members of each UBO block instance
        for (const RHIShader::UBO_Member &uboInstance : std::as_const(block->members)) {
            uploadShaderDataProperty(shaderData, materialsUBO,
                                     uboInstance, distanceToCommand);
        }
    }

    // Note: There's nothing to do for SSBO as those are directly uploaded to the GPU, no extracting
    // required
}

HRHIBuffer PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize::bufferForCommand(size_t distanceToCommand) const
{
    const size_t uboIdx = distanceToCommand / commandsPerUBO;
    return buffers[uboIdx];
}

size_t PipelineUBOSet::MultiUBOBufferWithBindingAndBlockSize::localOffsetInBufferForCommand(size_t distanceToCommand) const
{
    return (distanceToCommand % commandsPerUBO) * alignedBlockSize;
}

} // Rhi

} // Render

} // Qt3DRender

QT_END_NAMESPACE
