Skip to main content
added 9149 characters in body
Source Link

Current setup:

UserInterfaceElement.h

struct UserInterfaceElement
{
    std::unique_ptr<UserInterfaceRenderer> renderer;
    std::unique_ptr<UiCache> cache;
    Texture texture;
    Model model;
    QString qmlPath;
};

UserInterfaceRenderer.h

class UserInterfaceRenderer : public QObject
{
    Q_OBJECT
public:
    UserInterfaceRenderer(QWindow* parentWindow = nullptr);
    ~UserInterfaceRenderer();

    void loadQml(const QSize& size, const QString& qmlPath);
    void render();
    void resize(const QSize& size);
    void createFbo(const QSize& size);
    void deleteFbo();
    QImage grabImage(); // taking screenshot
    QOpenGLFramebufferObject* getFbo() const { return fbo; }
    QQuickWindow* getQuickWindow() const { return quickWindow; }

    void forwardEvent(QEvent* event);
    QQuickItem* getRootItem() const { return rootItem; }

private:
    QQuickRenderControl* renderControl = nullptr;
    QQuickWindow* quickWindow = nullptr;
    QQmlEngine* engine = nullptr;
    QQmlComponent* component = nullptr;
    QQuickItem* rootItem = nullptr;

    QOpenGLFramebufferObject* fbo = nullptr;
    QOpenGLContext* context = nullptr;
    QOffscreenSurface* offscreenSurface = nullptr;
    QSize surfaceSize;
};

UserInterfaceRenderer.cpp

UserInterfaceRenderer::UserInterfaceRenderer(QWindow* parentWindow) {
    renderControl = new QQuickRenderControl(this);
    quickWindow = new QQuickWindow(renderControl);
    quickWindow->setGraphicsApi(QSGRendererInterface::OpenGL);
    quickWindow->setFlags(
        Qt::FramelessWindowHint | Qt::Tool | 
        Qt::WindowStaysOnTopHint | Qt::WindowTransparentForInput
    );
    quickWindow->setColor(Qt::transparent);

    context = new QOpenGLContext(parentWindow);
    context->setFormat(parentWindow->format());
    context->create();

    offscreenSurface = new QOffscreenSurface();
    offscreenSurface->setFormat(context->format());
    offscreenSurface->create();

    context->makeCurrent(offscreenSurface);
    quickWindow->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(context));
    renderControl->initialize();
    quickWindow->create();

    //quickWindow->setTransientParent(parentWindow);
    //quickWindow->setParent(parentWindow);

    engine = new QQmlEngine(this);
    if (!engine->incubationController()) {
        engine->setIncubationController(quickWindow->incubationController());
    }
}

UserInterfaceRenderer::~UserInterfaceRenderer() {
    if (rootItem) {
        rootItem->setParentItem(nullptr);
        delete rootItem;
    }

    delete renderControl;
    delete quickWindow;
    delete engine;
    delete component;
    deleteFbo();

    if (context) {
        context->doneCurrent();
        delete context;
    }

    if (offscreenSurface) {
        offscreenSurface->destroy();
        delete offscreenSurface;
    }
}

void UserInterfaceRenderer::forwardEvent(QEvent* event) {
    QCoreApplication::sendEvent(quickWindow, event);
}

void UserInterfaceRenderer::loadQml(const QSize& size, const QString& qmlPath) {
    component = new QQmlComponent(engine, QUrl::fromLocalFile(qmlPath));

    rootItem = qobject_cast<QQuickItem*>(component->create());
    if (!rootItem) {
        qWarning() << "[UserInterfaceRenderer] Failed to load QML root item." << qmlPath;
        return;
    }

    rootItem->setParentItem(quickWindow->contentItem());
    rootItem->setSize(size);
    quickWindow->resize(size);

    surfaceSize = size;

    deleteFbo();
    createFbo(size);
}

void UserInterfaceRenderer::resize(const QSize& size) {
    if (!rootItem) return;

    rootItem->setSize(size);
    quickWindow->resize(size);
    surfaceSize = size;

    deleteFbo();
    createFbo(size);
}

void UserInterfaceRenderer::createFbo(const QSize& size) {
    context->makeCurrent(offscreenSurface);

    QOpenGLFramebufferObjectFormat format;
    format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
    format.setTextureTarget(GL_TEXTURE_2D);
    format.setInternalTextureFormat(GL_RGBA8);
    fbo = new QOpenGLFramebufferObject(size, format);

    QQuickRenderTarget renderTarget = QQuickRenderTarget::fromOpenGLTexture(
        fbo->texture(),
        surfaceSize
    );
    quickWindow->setRenderTarget(renderTarget);
}

void UserInterfaceRenderer::deleteFbo()
{
    if (fbo) {
        context->makeCurrent(offscreenSurface);
        delete fbo;
        fbo = nullptr;
    }
}

void UserInterfaceRenderer::render() {
    if (!rootItem || !quickWindow || !fbo) return;

    if (!context->makeCurrent(offscreenSurface)) {
        qWarning() << "Failed to make OpenGL context current!";
        return;
    }

    QOpenGLFunctions* f = context->functions();
    f->glViewport(0, 0, surfaceSize.width(), surfaceSize.height());
    f->glClearColor(0, 0, 0, 0); // transparent clear
    f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    renderControl->beginFrame();
    renderControl->polishItems();
    renderControl->sync();
    renderControl->render();
    renderControl->endFrame();

    f->glFlush();
}

Model.h

struct Texture {
    VkImage                                 image         = VK_NULL_HANDLE;
    VmaAllocation                           vmaAllocation = VK_NULL_HANDLE;
    VkImageView                             imageView     = VK_NULL_HANDLE;
    VkSampler                               sampler       = VK_NULL_HANDLE;
    uint32_t                                mipLevels     = 0;
    uint32_t                                width         = 0;
    uint32_t                                height        = 0;
};

struct Material {
    Texture                                 diffuseTexture; // basic color
    Texture                                 normalTexture;
    Texture                                 specularTexture;
    Texture                                 emissiveTexture;
};

struct Mesh
{
    std::vector<Vertex>                     vertices;
    std::vector<uint32_t>                   indices;
    glm::mat4                               transform              = glm::mat4(1.0f);

    Material                                material;

    VkBuffer                                vertexBuffer           = VK_NULL_HANDLE;
    VmaAllocation                           vertexBufferAllocation = VK_NULL_HANDLE;
    VkBuffer                                indexBuffer            = VK_NULL_HANDLE;
    VmaAllocation                           indexBufferAllocation  = VK_NULL_HANDLE;

    std::vector<VkDescriptorSet>            descriptorSets         = std::vector<VkDescriptorSet>(MAX_FRAMES_IN_FLIGHT, VK_NULL_HANDLE);
};

struct Model {
    std::vector<Mesh>                       meshes;
    ModelType                               type = ModelType::OTHER;

    glm::vec3                               position;
    glm::vec3                               scale;
    glm::quat                               rotation;

    bool                                    isCollidable = false;
};

called in recordCommandBuffer function, which is called each frame for each UI element

void AetherEngine::recordUiElementToCommandBuffer(UserInterfaceElement& uiElement, VkCommandBuffer commandBuffer)
{
    renderQmlToTexture(uiElement.renderer.get(), uiElement.texture);
    vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines["ui"]);
    recordModelToCommandBuffer(uiElement.model, commandBuffer);
}
void AetherEngine::renderQmlToTexture(UserInterfaceRenderer* renderer, Texture& texture)
{
    renderer->render();
    QImage image = renderer->getFbo()->toImage().convertToFormat(QImage::Format_RGBA8888);

    if (texture.width != static_cast<uint32_t>(image.width()) ||
        texture.height != static_cast<uint32_t>(image.height())) {
        cleanupTexture(texture);

        modelManager.createSolidColorTexture({ 0, 0, 0, 0 }, image.width(), image.height(), texture);
    }

    modelManager.uploadRawDataToTexture(image.bits(), image.width(), image.height(), texture);
}

Current setup:

UserInterfaceElement.h

struct UserInterfaceElement
{
    std::unique_ptr<UserInterfaceRenderer> renderer;
    std::unique_ptr<UiCache> cache;
    Texture texture;
    Model model;
    QString qmlPath;
};

UserInterfaceRenderer.h

class UserInterfaceRenderer : public QObject
{
    Q_OBJECT
public:
    UserInterfaceRenderer(QWindow* parentWindow = nullptr);
    ~UserInterfaceRenderer();

    void loadQml(const QSize& size, const QString& qmlPath);
    void render();
    void resize(const QSize& size);
    void createFbo(const QSize& size);
    void deleteFbo();
    QImage grabImage(); // taking screenshot
    QOpenGLFramebufferObject* getFbo() const { return fbo; }
    QQuickWindow* getQuickWindow() const { return quickWindow; }

    void forwardEvent(QEvent* event);
    QQuickItem* getRootItem() const { return rootItem; }

private:
    QQuickRenderControl* renderControl = nullptr;
    QQuickWindow* quickWindow = nullptr;
    QQmlEngine* engine = nullptr;
    QQmlComponent* component = nullptr;
    QQuickItem* rootItem = nullptr;

    QOpenGLFramebufferObject* fbo = nullptr;
    QOpenGLContext* context = nullptr;
    QOffscreenSurface* offscreenSurface = nullptr;
    QSize surfaceSize;
};

UserInterfaceRenderer.cpp

UserInterfaceRenderer::UserInterfaceRenderer(QWindow* parentWindow) {
    renderControl = new QQuickRenderControl(this);
    quickWindow = new QQuickWindow(renderControl);
    quickWindow->setGraphicsApi(QSGRendererInterface::OpenGL);
    quickWindow->setFlags(
        Qt::FramelessWindowHint | Qt::Tool | 
        Qt::WindowStaysOnTopHint | Qt::WindowTransparentForInput
    );
    quickWindow->setColor(Qt::transparent);

    context = new QOpenGLContext(parentWindow);
    context->setFormat(parentWindow->format());
    context->create();

    offscreenSurface = new QOffscreenSurface();
    offscreenSurface->setFormat(context->format());
    offscreenSurface->create();

    context->makeCurrent(offscreenSurface);
    quickWindow->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(context));
    renderControl->initialize();
    quickWindow->create();

    //quickWindow->setTransientParent(parentWindow);
    //quickWindow->setParent(parentWindow);

    engine = new QQmlEngine(this);
    if (!engine->incubationController()) {
        engine->setIncubationController(quickWindow->incubationController());
    }
}

UserInterfaceRenderer::~UserInterfaceRenderer() {
    if (rootItem) {
        rootItem->setParentItem(nullptr);
        delete rootItem;
    }

    delete renderControl;
    delete quickWindow;
    delete engine;
    delete component;
    deleteFbo();

    if (context) {
        context->doneCurrent();
        delete context;
    }

    if (offscreenSurface) {
        offscreenSurface->destroy();
        delete offscreenSurface;
    }
}

void UserInterfaceRenderer::forwardEvent(QEvent* event) {
    QCoreApplication::sendEvent(quickWindow, event);
}

void UserInterfaceRenderer::loadQml(const QSize& size, const QString& qmlPath) {
    component = new QQmlComponent(engine, QUrl::fromLocalFile(qmlPath));

    rootItem = qobject_cast<QQuickItem*>(component->create());
    if (!rootItem) {
        qWarning() << "[UserInterfaceRenderer] Failed to load QML root item." << qmlPath;
        return;
    }

    rootItem->setParentItem(quickWindow->contentItem());
    rootItem->setSize(size);
    quickWindow->resize(size);

    surfaceSize = size;

    deleteFbo();
    createFbo(size);
}

void UserInterfaceRenderer::resize(const QSize& size) {
    if (!rootItem) return;

    rootItem->setSize(size);
    quickWindow->resize(size);
    surfaceSize = size;

    deleteFbo();
    createFbo(size);
}

void UserInterfaceRenderer::createFbo(const QSize& size) {
    context->makeCurrent(offscreenSurface);

    QOpenGLFramebufferObjectFormat format;
    format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
    format.setTextureTarget(GL_TEXTURE_2D);
    format.setInternalTextureFormat(GL_RGBA8);
    fbo = new QOpenGLFramebufferObject(size, format);

    QQuickRenderTarget renderTarget = QQuickRenderTarget::fromOpenGLTexture(
        fbo->texture(),
        surfaceSize
    );
    quickWindow->setRenderTarget(renderTarget);
}

void UserInterfaceRenderer::deleteFbo()
{
    if (fbo) {
        context->makeCurrent(offscreenSurface);
        delete fbo;
        fbo = nullptr;
    }
}

void UserInterfaceRenderer::render() {
    if (!rootItem || !quickWindow || !fbo) return;

    if (!context->makeCurrent(offscreenSurface)) {
        qWarning() << "Failed to make OpenGL context current!";
        return;
    }

    QOpenGLFunctions* f = context->functions();
    f->glViewport(0, 0, surfaceSize.width(), surfaceSize.height());
    f->glClearColor(0, 0, 0, 0); // transparent clear
    f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    renderControl->beginFrame();
    renderControl->polishItems();
    renderControl->sync();
    renderControl->render();
    renderControl->endFrame();

    f->glFlush();
}

Model.h

struct Texture {
    VkImage                                 image         = VK_NULL_HANDLE;
    VmaAllocation                           vmaAllocation = VK_NULL_HANDLE;
    VkImageView                             imageView     = VK_NULL_HANDLE;
    VkSampler                               sampler       = VK_NULL_HANDLE;
    uint32_t                                mipLevels     = 0;
    uint32_t                                width         = 0;
    uint32_t                                height        = 0;
};

struct Material {
    Texture                                 diffuseTexture; // basic color
    Texture                                 normalTexture;
    Texture                                 specularTexture;
    Texture                                 emissiveTexture;
};

struct Mesh
{
    std::vector<Vertex>                     vertices;
    std::vector<uint32_t>                   indices;
    glm::mat4                               transform              = glm::mat4(1.0f);

    Material                                material;

    VkBuffer                                vertexBuffer           = VK_NULL_HANDLE;
    VmaAllocation                           vertexBufferAllocation = VK_NULL_HANDLE;
    VkBuffer                                indexBuffer            = VK_NULL_HANDLE;
    VmaAllocation                           indexBufferAllocation  = VK_NULL_HANDLE;

    std::vector<VkDescriptorSet>            descriptorSets         = std::vector<VkDescriptorSet>(MAX_FRAMES_IN_FLIGHT, VK_NULL_HANDLE);
};

struct Model {
    std::vector<Mesh>                       meshes;
    ModelType                               type = ModelType::OTHER;

    glm::vec3                               position;
    glm::vec3                               scale;
    glm::quat                               rotation;

    bool                                    isCollidable = false;
};

called in recordCommandBuffer function, which is called each frame for each UI element

void AetherEngine::recordUiElementToCommandBuffer(UserInterfaceElement& uiElement, VkCommandBuffer commandBuffer)
{
    renderQmlToTexture(uiElement.renderer.get(), uiElement.texture);
    vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines["ui"]);
    recordModelToCommandBuffer(uiElement.model, commandBuffer);
}
void AetherEngine::renderQmlToTexture(UserInterfaceRenderer* renderer, Texture& texture)
{
    renderer->render();
    QImage image = renderer->getFbo()->toImage().convertToFormat(QImage::Format_RGBA8888);

    if (texture.width != static_cast<uint32_t>(image.width()) ||
        texture.height != static_cast<uint32_t>(image.height())) {
        cleanupTexture(texture);

        modelManager.createSolidColorTexture({ 0, 0, 0, 0 }, image.width(), image.height(), texture);
    }

    modelManager.uploadRawDataToTexture(image.bits(), image.width(), image.height(), texture);
}

How to integrate QML UI into a custom Vulkan renderer without using a separate window

I'm developing a custom Vulkan renderer and want to integrate a QML-based UI into it.

I already have a working Vulkan setup and also managed to render QML over Vulkan using a separate QQuickWindow and QQuickRenderControl, but this approach isn't ideal - the QML elements live in a distinct window, so they have their own focus and input handling. I can use QCoreApplication::sendEvent to send events, which works for Buttons, but doesn't work for TextField, because for TextFields the underlying window should be activated, but as I said currently my UI elements have distinct QQuickWindows and I don't want to activate them, so that my QWindow I use to render Vulkan onto won't lose focus.

What I want instead, is for QML elements to share focus and input events with my in-game window (i.e. no separate OS-level window, a single rendering surface that draws both game content and QML UI).

Is there a way to embed QML rendering directly into an existing Vulkan surface or swapchain image, so that both systems share focus and input?

Any examples or recommended architecture?