lxz

lxz

C++ | Typescript | 图形白痴

Part.1 如何實現一個基於 wlroots 的合成器

名詞概念#

Qt#

Qt(/ˈkjuːt/,發音同 “cute”)是一個跨平台的 C++ 應用程序開發框架。廣泛用於開發 GUI 程序,這種情況下又被稱為部件工具箱。也可用於開發非 GUI 程序,例如控制台工具和伺服器。

wlroots#

用於構建 Wayland 合成器的模組化工具集,簡化了約 60,000 行代碼的開發工作。

  • 提供抽象底層顯示和輸入的後端,支持 KMS/DRM、libinput、Wayland、X11 等,可動態創建和銷毀。
  • 實現多種 Wayland 接口,支持協議擴展,促進合成器標準化。
  • 提供通用合成器組件,如物理空間輸出管理。
  • 集成 Xwayland 抽象,簡化 X11 窗口管理。
  • 提供渲染器抽象,支持簡單和自定義渲染需求。

seat#

由分配給特定工作場景的所有硬體設備組成。它至少包含一個圖形設備,通常還有鍵盤和滑鼠。此外,它可能包括攝像頭、聲卡等設備。座位由座位名稱標識,這是一個短字符串(不超過 64 個字符),以 "seat" 四個字符開頭,後跟至少一個 a-zA-Z0-9 範圍內的字符,或 "_" 和 "-"。這種命名方式適合用於檔案名。座位名稱可能是穩定的,也可能不穩定,如果座位再次可用,其名稱可以重複使用。

RHI#

RHI 是 Renderer Hardware Interface(渲染硬體接口)的縮寫,是一套對硬體的抽象,在上層只需要設置參數,底層具體使用的是 OpenGL、Vulkan、DX12 還是 Metal 哪套接口,我們是不必關心的。

Qt6 提供了 QRHI,為 Qt 程序提供了底層的硬體抽象,這樣上層的 QtQuick 組件在執行 GPU 渲染時,就可以自動調用對應的驅動接口。

QPA#

Qt 平台抽象(QPA)是 Qt 中的核心平台抽象層。

QPA 的 API 可通過類前綴 "QPlatform*" 識別,用於實現 Qt GUI 中的高級類。例如,QPlatformWindow 用於窗口系統集成,而 QPlatformThemeQStyleHint 則用於深層次的平台主題和集成。

基本工作流程#

Treeland 使用 QQuickWindow 作為渲染的根,這樣在 Treeland 裡開發時,就如同開發一個普通 Qt 程序一樣,先創建一個 Window,在 Window 內創建 Qt 控件,使用 QEvent 處理各種事件。

那麼 Treeland 是如何實現這件事的呢?

QQuickWindow 的私有類提供了自定義 QQuickGraphicsDevice 對象的接口,而 QQuickGraphicsDevice 可以使用 fromOpenGLContextfromPhyicalDevice 創建新的對象,那麼 Treeland 只需要繼承 QQuickWindow,並從 wlroots 獲取 OpenGL context 和 phyical device,就可以將 Qt QuickWindow 的渲染,嫁接到 wlroots 上。

通過將 wlroots 的渲染上下文與 Qt 的渲染上下文進行結合,可以將 wlroots 渲染的圖形結果嵌入到 Qt 應用程序的渲染流程中,可以直接使用 wlroots 提供的圖形資源和設備對象,如物理設備(phdev)、邏輯設備(dev)和隊列族(queue_family),以減少不必要的上下文切換和資源拷貝。這樣,Qt 就可以利用 wlroots 提供的渲染能力,同時能夠繼續使用 Qt 的渲染框架和 API。

之後在 Qt QPA 中將螢幕信息,以及輸入信息轉換成 Qt 內部對象,從而利用 Qt 自身的事件循環等機制繼續處理。

Qt QPA#

QPA 為 Qt 提供了跨平台的接口抽象能力,我們可以提供自己的 QPA 插件來為 Qt 程序提供新的能力,例如將 wlroots 的輸入事件轉換成 Qt 內部事件。

輸入事件處理#

Treeland 處理底層事件與上層事件的流程

bool WOutputRenderWindow::event(QEvent *event)
{
    Q_D(WOutputRenderWindow);

    if (event->type() == doRenderEventType) {
        QCoreApplication::removePostedEvents(this, doRenderEventType);
        d_func()->doRender();
        return true;
    }

    if (QW::RenderWindow::beforeDisposeEventFilter(this, event)) {
        event->accept();
        QW::RenderWindow::afterDisposeEventFilter(this, event);
        return true;
    }

    bool isAccepted = QQuickWindow::event(event);
    if (QW::RenderWindow::afterDisposeEventFilter(this, event))
        return true;

    return isAccepted;
}

WOutputRenderWindow 的事件處理中,會額外調用下 seat 的事件過濾器,確保合成器可以攔截掉一部分事件,例如將一部分按鍵攔截下來,不發送給客戶端。

bool QWlrootsRenderWindow::beforeDisposeEventFilter(QEvent *event)
{
    if (event->isInputEvent()) {
        auto ie = static_cast<QInputEvent*>(event);
        auto device = WInputDevice::from(ie->device());
        Q_ASSERT(device);
        Q_ASSERT(device->seat());
        lastActiveCursor = device->seat()->cursor();
        return device->seat()->filterEventBeforeDisposeStage(window(), ie);
    }

    return false;
}

這段代碼展示了轉換輸入設備的功能,判斷輸入設備的類型,創建對應的 QInputDevice 對象。

QPointer<QInputDevice> QWlrootsIntegration::addInputDevice(WInputDevice *device, const QString &seatName)
{
    QPointer<QInputDevice> qtdev;
    auto qwDevice = device->handle();
    const QString name = QString::fromUtf8(qwDevice->handle()->name);
    qint64 systemId = reinterpret_cast<qint64>(device);

    switch (qwDevice->handle()->type) {
    case WLR_INPUT_DEVICE_KEYBOARD: {
        qtdev = new QInputDevice(name, systemId, QInputDevice::DeviceType::Keyboard, seatName);
        break;
    }
    case WLR_INPUT_DEVICE_POINTER: {
        qtdev = new QPointingDevice(name, systemId, QInputDevice::DeviceType::TouchPad, QPointingDevice::PointerType::Generic,
                                    QInputDevice::Capability::Position | QInputDevice::Capability::Hover
                                        | QInputDevice::Capability::Scroll | QInputDevice::Capability::MouseEmulation,
                                    10, 32, seatName, QPointingDeviceUniqueId());
        break;
    }
    case WLR_INPUT_DEVICE_TOUCH: {
        qtdev = new QPointingDevice(name, systemId, QInputDevice::DeviceType::TouchScreen, QPointingDevice::PointerType::Finger,
                                    QInputDevice::Capability::Position | QInputDevice::Capability::Area | QInputDevice::Capability::MouseEmulation,
                                    10, 32, seatName, QPointingDeviceUniqueId());
        break;
    }
    case WLR_INPUT_DEVICE_TABLET_TOOL: {
        qtdev = new QPointingDevice(name, systemId, QInputDevice::DeviceType::Stylus, QPointingDevice::PointerType::Pen,
                                    QInputDevice::Capability::XTilt | QInputDevice::Capability::YTilt | QInputDevice::Capability::Pressure,
                                    1, 32, seatName, QPointingDeviceUniqueId());
        break;
    }
    case WLR_INPUT_DEVICE_TABLET_PAD: {
        auto pad = wlr_tablet_pad_from_input_device(qwDevice->handle());
        qtdev = new QPointingDevice(name, systemId, QInputDevice::DeviceType::TouchPad, QPointingDevice::PointerType::Pen,
                                    QInputDevice::Capability::Position | QInputDevice::Capability::Hover | QInputDevice::Capability::Pressure,
                                    1, pad->button_count, seatName, QPointingDeviceUniqueId());
        break;
    }
    case WLR_INPUT_DEVICE_SWITCH: {
        qtdev = new QInputDevice(name, systemId, QInputDevice::DeviceType::Keyboard, seatName);
        break;
    }
    }

    if (qtdev) {
        device->setQtDevice(qtdev);
        QWindowSystemInterface::registerInputDevice(qtdev);

        if (qtdev->type() == QInputDevice::DeviceType::Mouse || qtdev->type() == QInputDevice::DeviceType::TouchPad) {
            auto primaryQtDevice = QPointingDevice::primaryPointingDevice();
            if (!WInputDevice::from(primaryQtDevice)) {
                // Ensure the primary pointing device is the WInputDevice
                auto pd = const_cast<QPointingDevice*>(primaryQtDevice);
                pd->setParent(nullptr);
                delete pd;
            }
            Q_ASSERT(WInputDevice::from(QPointingDevice::primaryPointingDevice()));
        } else if (qtdev->type() == QInputDevice::DeviceType::Keyboard) {
            auto primaryQtDevice = QInputDevice::primaryKeyboard();
            if (!WInputDevice::from(primaryQtDevice)) {
                // Ensure the primary keyboard device is the WInputDevice
                auto pd = const_cast<QInputDevice*>(primaryQtDevice);
                pd->setParent(nullptr);
                delete pd;
            }
            Q_ASSERT(WInputDevice::from(QInputDevice::primaryKeyboard()));
        }
    }

    return qtdev;
}

客戶端事件#

在 Treeland 還有一種事件需要處理,當用戶點擊一個窗口,合成器需要告知客戶端哪個坐標點擊了。或者使用鍵盤進行輸入時,需要告知客戶端輸入的內容。

首先,Treeland 會標記一個窗口成為激活窗口,設置給 seat,這樣 wlroots 就知道哪個窗口此時擁有焦點。

之後當鍵盤發生輸入事件時,Treeland 沒有過濾掉按鍵事件,或者是放行某些按鍵,這些剩餘的輸入事件就會在 wseat 的 sendEvent 中,發送給激活的客戶端。

// for keyboard event
inline bool doNotifyKey(WInputDevice *device, uint32_t keycode, uint32_t state, uint32_t timestamp) {
    if (!keyboardFocusSurface())
        return false;

    q_func()->setKeyboard(device);
    /* Send modifiers to the client. */
    this->handle()->keyboard_notify_key(timestamp, keycode, state);
    return true;
}

螢幕信息#

在 QPA 中還對 WOutput 進行了封裝 QWlrootsScreen

QWlrootsScreen *QWlrootsIntegration::addScreen(WOutput *output)
{
    m_screens << new QWlrootsScreen(output);

    if (isMaster()) {
        QWindowSystemInterface::handleScreenAdded(m_screens.last());

        if (m_placeholderScreen) {
            QWindowSystemInterface::handleScreenRemoved(m_placeholderScreen.release());
        }
    } else {
        Q_UNUSED(new QScreen(m_screens.last()))
    }

    m_screens.last()->initialize();
    output->setScreen(m_screens.last());

    return m_screens.last();
}

QWlrootsScreen 繼承自 QPlatformScreen,做的事情是將部分參數進行轉換,例如 physicalSize、devicePixelRatio、DPI 等,之後通過 QWindowSystemInterface::handleScreenAdded 將創建好的 QWlrootsScreen 添加進 Qt 內。

Qt RHI#

摘抄一段來自 waylib 中初始化 Qt RHI 的代碼

bool WOutputRenderWindowPrivate::initRCWithRhi()
{
    W_Q(WOutputRenderWindow);

    QQuickRenderControlPrivate *rcd = QQuickRenderControlPrivate::get(rc());
    QSGRhiSupport *rhiSupport = QSGRhiSupport::instance();

// sanity check for Vulkan
#ifdef ENABLE_VULKAN_RENDER
    if (rhiSupport->rhiBackend() == QRhi::Vulkan) {
        vkInstance.reset(new QVulkanInstance());

        auto phdev = wlr_vk_renderer_get_physical_device(m_renderer->handle());
        auto dev = wlr_vk_renderer_get_device(m_renderer->handle());
        auto queue_family = wlr_vk_renderer_get_queue_family(m_renderer->handle());

#if QT_VERSION > QT_VERSION_CHECK(6, 6, 0)
        auto instance = wlr_vk_renderer_get_instance(m_renderer->handle());
        vkInstance->setVkInstance(instance);
#endif
        //        vkInstance->setExtensions(fromCStyleList(vkRendererAttribs.extension_count, vkRendererAttribs.extensions));
        //        vkInstance->setLayers(fromCStyleList(vkRendererAttribs.layer_count, vkRendererAttribs.layers));
        vkInstance->setApiVersion({1, 1, 0});
        vkInstance->create();
        q->setVulkanInstance(vkInstance.data());

        auto gd = QQuickGraphicsDevice::fromDeviceObjects(phdev, dev, queue_family);
        q->setGraphicsDevice(gd);
    } else
#endif
    if (rhiSupport->rhiBackend() == QRhi::OpenGLES2) {
        Q_ASSERT(wlr_renderer_is_gles2(m_renderer->handle()));
        auto egl = wlr_gles2_renderer_get_egl(m_renderer->handle());
        auto display = wlr_egl_get_display(egl);
        auto context = wlr_egl_get_context(egl);

        this->glContext = new QW::OpenGLContext(display, context, rc());
        bool ok = this->glContext->create();
        if (!ok)
            return false;

        q->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(this->glContext));
    } else {
        return false;
    }

    QOffscreenSurface *offscreenSurface = new QW::OffscreenSurface(nullptr, q);
    offscreenSurface->create();

    QSGRhiSupport::RhiCreateResult result = rhiSupport->createRhi(q, offscreenSurface);
    if (!result.rhi) {
        qWarning("WOutput::initRhi: Failed to initialize QRhi");
        return false;
    }

    rcd->rhi = result.rhi;
    // Ensure the QQuickRenderControl don't reinit the RHI
    rcd->ownRhi = true;
    if (!rc()->initialize())
        return false;
    rcd->ownRhi = result.own;
    Q_ASSERT(rcd->rhi == result.rhi);
    Q_ASSERT(!swapchain);

    return true;
}

先獲取 QSGRhiSupport 及相關控制對象。

判斷 RHI backend 的類型,需要適配 vulkangles等。

wlroots 獲取物理設備等參數,使用 QQuickGraphicsDevice::fromDeviceObjects 創建 Qt 的 QQuickGraphicsDevice

render window 的私有類是繼承自 QQuickWindowPrivate,只需要將獲取到的 QQuickGraphicsDevice 設置給 QQuickWindowPrivate::setGraphicsDevice 即可。

之後創建一個離屏渲染表面,用於 RHI 的初始化。

Qt Viewport#

在 Qt 中,想要查看或者渲染一個組件,需要使用 Viewport 組件,俗稱照相機。

視口(Viewport)是一個可觀察的多邊形區域,只有 Viewport 範圍內的畫面才能顯示到螢幕上。

wlroots 中的 Viewport 是一個與 Wayland 顯示協議相關的概念,主要用於定義渲染輸出在螢幕上的顯示區域。它允許在渲染時對顯示內容進行縮放、裁剪或平移,以適應不同的解析度和顯示需求。

Treeland 使用 WOutputViewport 提供 Viewport 功能,使用 wlrootswlr_output 中的螢幕信息,對畫面進行矩陣變換,這裡會涉及到螢幕的縮放、DPI 等參數。

QMatrix4x4 WOutputViewport::renderMatrix() const
{
    QMatrix4x4 renderMatrix;

    if (auto customTransform = viewportTransform()) {
        customTransform->applyTo(&renderMatrix);
    } else if (parentItem() && !ignoreViewport() && input() != this) {
        auto d = QQuickItemPrivate::get(const_cast<WOutputViewport*>(this));
        auto viewportMatrix = d->itemNode()->matrix().inverted();
        if (auto inputItem = input()) {
            QMatrix4x4 matrix = QQuickItemPrivate::get(parentItem())->itemToWindowTransform();
            matrix *= QQuickItemPrivate::get(inputItem)->windowToItemTransform();
            renderMatrix = viewportMatrix * matrix.inverted();
        } else { // the input item is window's contentItem
            auto pd = QQuickItemPrivate::get(parentItem());
            QMatrix4x4 parentMatrix = pd->itemToWindowTransform().inverted();
            renderMatrix = viewportMatrix * parentMatrix;
        }
    }

    return renderMatrix;
}

WOutputViewport 提供了 Viewport 所需的所有參數,變換矩陣、源幾何大小、目標幾何大小等信息。

WOutputRenderWindow 的事件中,判斷如果是渲染的事件,就執行渲染。

bool WOutputRenderWindow::event(QEvent *event)
{
    Q_D(WOutputRenderWindow);

    if (event->type() == doRenderEventType) {
        QCoreApplication::removePostedEvents(this, doRenderEventType);
        d_func()->doRender();
        return true;
    }

    if (QW::RenderWindow::beforeDisposeEventFilter(this, event)) {
        event->accept();
        QW::RenderWindow::afterDisposeEventFilter(this, event);
        return true;
    }

    bool isAccepted = QQuickWindow::event(event);
    if (QW::RenderWindow::afterDisposeEventFilter(this, event))
        return true;

    return isAccepted;
}

在 doRender 中,遍歷所有的 Output,執行 beginRender,然後執行 Output 的渲染。

void WOutputRenderWindowPrivate::doRender(const QList<OutputHelper *> &outputs,
                                          bool forceRender, bool doCommit)
{
    Q_ASSERT(rendererList.isEmpty());
    Q_ASSERT(!inRendering);
    inRendering = true;

    W_Q(WOutputRenderWindow);
    for (OutputLayer *layer : std::as_const(layers)) {
        layer->beforeRender(q);
    }

    rc()->polishItems();

    if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi()))
        rc()->beginFrame();
    rc()->sync();

    QQuickAnimatorController_advance(animationController.get());
    Q_EMIT q->beforeRendering();
    runAndClearJobs(&beforeRenderingJobs);

    auto needsCommit = doRenderOutputs(outputs, forceRender);

    Q_EMIT q->afterRendering();
    runAndClearJobs(&afterRenderingJobs);

    if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi()))
        rc()->endFrame();

    if (doCommit) {
        for (auto i : std::as_const(needsCommit)) {
            bool ok = i.first->commit(i.second);

            if (i.second->currentBuffer()) {
                i.second->endRender();
            }

            i.first->resetState(ok);
        }
    }

    resetGlState();

    // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all
    // outputs, and blit nvidia's output buffer in drm_connector_state_update_primary_fb,
    // the 'blit' behavior will make EGL context to Nvidia renderer. So must done current
    // OpenGL context here in order to ensure QtQuick always make EGL context to Intel
    // renderer before next frame.
    if (glContext)
        glContext->doneCurrent();

    inRendering = false;
    Q_EMIT q->renderEnd();
}
qw_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePixelRatio,
                                        uint32_t format, RenderFlags flags)
{
    Q_ASSERT(!state.buffer);
    Q_ASSERT(m_output);

    if (pixelSize.isEmpty())
        return nullptr;

    Q_EMIT beforeRendering();

    m_damageRing.set_bounds(pixelSize.width(), pixelSize.height());

    // configure swapchain
    if (flags.testFlag(RenderFlag::DontConfigureSwapchain)) {
        auto renderFormat = pickFormat(m_output->renderer(), format);
        if (!renderFormat) {
            qWarning("wlr_renderer doesn't support format 0x%s", drmGetFormatName(format));
            return nullptr;
        }

        if (!m_swapchain || QSize(m_swapchain->handle()->width, m_swapchain->handle()->height) != pixelSize
            || m_swapchain->handle()->format.format != renderFormat->format) {
            if (m_swapchain)
                delete m_swapchain;
            m_swapchain = qw_swapchain::create(m_output->allocator()->handle(), pixelSize.width(), pixelSize.height(), renderFormat);
        }
    } else if (flags.testFlag(RenderFlag::UseCursorFormats)) {
        bool ok = m_output->configureCursorSwapchain(pixelSize, format, &m_swapchain);
        if (!ok)
            return nullptr;
    } else {
        bool ok = m_output->configurePrimarySwapchain(pixelSize, format, &m_swapchain,
                                                      !flags.testFlag(DontTestSwapchain));
        if (!ok)
            return nullptr;
    }

    // TODO: Support scanout buffer of wlr_surface(from WSurfaceItem)
    int bufferAge;
    auto wbuffer = m_swapchain->acquire(&bufferAge);
    if (!wbuffer)
        return nullptr;
    auto buffer = qw_buffer::from(wbuffer);

    if (!m_renderHelper)
        m_renderHelper = new WRenderHelper(m_output->renderer());
    m_renderHelper->setSize(pixelSize);

    auto wd = QQuickWindowPrivate::get(window());
    Q_ASSERT(wd->renderControl);
    auto lastRT = m_renderHelper->lastRenderTarget();
    auto rt = m_renderHelper->acquireRenderTarget(wd->renderControl, buffer);
    if (rt.isNull()) {
        buffer->unlock();
        return nullptr;
    }

    auto rtd = QQuickRenderTargetPrivate::get(&rt);
    QSGRenderTarget sgRT;

    if (rtd->type == QQuickRenderTargetPrivate::Type::PaintDevice) {
        sgRT.paintDevice = rtd->u.paintDevice;
    } else {
        Q_ASSERT(rtd->type == QQuickRenderTargetPrivate::Type::RhiRenderTarget);
        sgRT.rt = rtd->u.rhiRt;
        sgRT.cb = wd->redirect.commandBuffer;
        Q_ASSERT(sgRT.cb);
        sgRT.rpDesc = rtd->u.rhiRt->renderPassDescriptor();

#ifndef QT_NO_OPENGL
        if (wd->rhi->backend() == QRhi::OpenGLES2) {
            auto glRT = QRHI_RES(QGles2TextureRenderTarget, rtd->u.rhiRt);
            Q_ASSERT(glRT->framebuffer >= 0);
            auto glContext = QOpenGLContext::currentContext();
            Q_ASSERT(glContext);
            QOpenGLContextPrivate::get(glContext)->defaultFboRedirect = glRT->framebuffer;
        }
#endif
    }

    state.flags = flags;
    state.context = wd->context;
    state.pixelSize = pixelSize;
    state.devicePixelRatio = devicePixelRatio;
    state.bufferAge = bufferAge;
    state.lastRT = lastRT;
    state.buffer = buffer;
    state.renderTarget = rt;
    state.sgRenderTarget = sgRT;

    return buffer;
}
QVector<std::pair<OutputHelper*, WBufferRenderer*>>
WOutputRenderWindowPrivate::doRenderOutputs(const QList<OutputHelper*> &outputs, bool forceRender)
{
    QVector<OutputHelper*> renderResults;
    renderResults.reserve(outputs.size());
    for (OutputHelper *helper : std::as_const(outputs)) {
        if (Q_LIKELY(!forceRender)) {
            if (!helper->renderable()
                || Q_UNLIKELY(!WOutputViewportPrivate::get(helper->output())->renderable())
                || !helper->output()->output()->isEnabled())
                continue;

            if (!helper->contentIsDirty()) {
                if (helper->needsFrame())
                    renderResults.append(helper);
                continue;
            }
        }

        Q_ASSERT(helper->output()->output()->scale() <= helper->output()->devicePixelRatio());

        const auto &format = helper->qwoutput()->handle()->render_format;
        const auto renderMatrix = helper->output()->renderMatrix();

        // maybe using the other WOutputViewport's QSGTextureProvider
        if (!helper->output()->depends().isEmpty())
            updateDirtyNodes();

        qw_buffer *buffer = helper->beginRender(helper->bufferRenderer(), helper->output()->output()->size(), format,
                                                WBufferRenderer::RedirectOpenGLContextDefaultFrameBufferObject);
        Q_ASSERT(buffer == helper->bufferRenderer()->currentBuffer());
        if (buffer) {
            helper->render(helper->bufferRenderer(), 0, renderMatrix,
                           helper->output()->effectiveSourceRect(),
                           helper->output()->targetRect(),
                           helper->output()->preserveColorContents());
        }
        renderResults.append(helper);
    }

    QVector<std::pair<OutputHelper*, WBufferRenderer*>> needsCommit;
    needsCommit.reserve(renderResults.size());
    for (auto helper : std::as_const(renderResults)) {
        auto bufferRenderer = helper->afterRender();
        if (bufferRenderer)
            needsCommit.append({helper, bufferRenderer});
    }

    rendererList.clear();

    return needsCommit;
}
void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix,
                             const QRectF &sourceRect, const QRectF &targetRect,
                             bool preserveColorContents)
{
    Q_ASSERT(state.buffer);

    const auto &source = m_sourceList.at(sourceIndex);
    QSGRenderer *renderer = ensureRenderer(sourceIndex, state.context);
    auto wd = QQuickWindowPrivate::get(window());

    const qreal devicePixelRatio = state.devicePixelRatio;
    state.renderer = renderer;
    state.worldTransform = renderMatrix;
    renderer->setDevicePixelRatio(devicePixelRatio);
    renderer->setDeviceRect(QRect(QPoint(0, 0), state.pixelSize));
    renderer->setRenderTarget(state.sgRenderTarget);
    const auto viewportRect = scaleToRect(targetRect, devicePixelRatio);

    auto softwareRenderer = dynamic_cast<QSGSoftwareRenderer*>(renderer);
    { // before render
        if (softwareRenderer) {
            // because software renderer don't supports viewportRect,
            // so use transform to simulation.
            const auto mapTransform = inputMapToOutput(sourceRect, targetRect,
                                                       state.pixelSize, state.devicePixelRatio);
            if (!mapTransform.isIdentity())
                state.worldTransform = mapTransform * state.worldTransform;
            state.worldTransform.optimize();
            auto image = getImageFrom(state.renderTarget);
            image->setDevicePixelRatio(devicePixelRatio);

            // TODO: Should set to QSGSoftwareRenderer, but it's not support specify matrix.
            // If transform is changed, it will full repaint.
            if (isRootItem(source.source)) {
                auto rootTransform = QQuickItemPrivate::get(wd->contentItem)->itemNode();
                if (rootTransform->matrix() != state.worldTransform)
                    rootTransform->setMatrix(state.worldTransform);
            } else {
                auto t = state.worldTransform.toTransform();
                if (t.type() > QTransform::TxTranslate) {
                    (image->operator QImage &()).fill(renderer->clearColor());
                    softwareRenderer->markDirty();
                }

                applyTransform(softwareRenderer, t);
            }
        } else {
            state.worldTransform.optimize();

            bool flipY = wd->rhi ? !wd->rhi->isYUpInNDC() : false;
            if (state.renderTarget.mirrorVertically())
                flipY = !flipY;

            if (viewportRect.isValid()) {
                QRect vr = viewportRect;
                if (flipY)
                    vr.moveTop(-vr.y() + state.pixelSize.height() - vr.height());
                renderer->setViewportRect(vr);
            } else {
                renderer->setViewportRect(QRect(QPoint(0, 0), state.pixelSize));
            }

            QRectF rect = sourceRect;
            if (!rect.isValid())
                rect = QRectF(QPointF(0, 0), QSizeF(state.pixelSize) / devicePixelRatio);

            const float left = rect.x();
            const float right = rect.x() + rect.width();
            float bottom = rect.y() + rect.height();
            float top = rect.y();

            if (flipY)
                std::swap(top, bottom);

            QMatrix4x4 matrix;
            matrix.ortho(left, right, bottom, top, 1, -1);

            QMatrix4x4 projectionMatrix, projectionMatrixWithNativeNDC;
            projectionMatrix = matrix * state.worldTransform;

            if (wd->rhi && !wd->rhi->isYUpInNDC()) {
                std::swap(top, bottom);

                matrix.setToIdentity();
                matrix.ortho(left, right, bottom, top, 1, -1);
            }
            projectionMatrixWithNativeNDC = matrix * state.worldTransform;

            renderer->setProjectionMatrix(projectionMatrix);
            renderer->setProjectionMatrixWithNativeNDC(projectionMatrixWithNativeNDC);

            auto textureRT = static_cast<QRhiTextureRenderTarget*>(state.sgRenderTarget.rt);
            if (preserveColorContents) {
                textureRT->setFlags(textureRT->flags() | QRhiTextureRenderTarget::PreserveColorContents);
            } else {
                textureRT->setFlags(textureRT->flags() & ~QRhiTextureRenderTarget::PreserveColorContents);
            }
        }
    }

    state.context->renderNextFrame(renderer);

    { // after render
        if (!softwareRenderer) {
            // TODO: get damage area from QRhi renderer
            m_damageRing.add_whole();
            // ###: maybe Qt bug? Before executing QRhi::endOffscreenFrame, we may
            // use the same QSGRenderer for multiple drawings. This can lead to
            // rendering the same content for different QSGRhiRenderTarget instances
            // when using the RhiGles backend. Additionally, considering that the
            // result of the current drawing may be needed when drawing the next
            // sourceIndex, we should let the RHI (Rendering Hardware Interface)
            // complete the results of this drawing here to ensure the current
            // drawing result is available for use.
            wd->rhi->finish();
        } else {
            auto currentImage = getImageFrom(state.renderTarget);
            Q_ASSERT(currentImage && currentImage == softwareRenderer->m_rt.paintDevice);
            currentImage->setDevicePixelRatio(1.0);
            const auto scaleTF = QTransform::fromScale(devicePixelRatio, devicePixelRatio);
            const auto scaledFlushRegion = scaleTF.map(softwareRenderer->flushRegion());
            PixmanRegion scaledFlushDamage;
            bool ok = WTools::toPixmanRegion(scaledFlushRegion, scaledFlushDamage);
            Q_ASSERT(ok);

            {
                PixmanRegion damage;
                m_damageRing.get_buffer_damage(state.bufferAge, damage);

                if (viewportRect.isValid()) {
                    QRect imageRect = (currentImage->operator const QImage &()).rect();
                    QRegion invalidRegion(imageRect);
                    invalidRegion -= viewportRect;
                    if (!scaledFlushRegion.isEmpty())
                        invalidRegion &= scaledFlushRegion;

                    if (!invalidRegion.isEmpty()) {
                        QPainter pa(currentImage);
                        for (const auto r : std::as_const(invalidRegion))
                            pa.fillRect(r, softwareRenderer->clearColor());
                    }
                }

                if (!damage.isEmpty() && state.lastRT.first != state.buffer && !state.lastRT.second.isNull()) {
                    auto image = getImageFrom(state.lastRT.second);
                    Q_ASSERT(image);
                    Q_ASSERT(image->size() == state.pixelSize);

                    // TODO: Don't use the previous render target, we can get the damage region of QtQuick
                    // before QQuickRenderControl::render for qw_damage_ring, and add dirty region to
                    // QSGAbstractSoftwareRenderer to force repaint the damage region of current render target.
                    QPainter pa(currentImage);

                    PixmanRegion remainderDamage;
                    ok = pixman_region32_subtract(remainderDamage, damage, scaledFlushDamage);
                    Q_ASSERT(ok);

                    int count = 0;
                    auto rects = pixman_region32_rectangles(remainderDamage, &count);
                    for (int i = 0; i < count; ++i) {
                        auto r = rects[i];
                        pa.drawImage(r.x1, r.y1, *image, r.x1, r.y1, r.x2 - r.x1, r.y2 - r.y1);
                    }
                }
            }

            if (!isRootItem(source.source))
                applyTransform(softwareRenderer, state.worldTransform.inverted().toTransform());
            m_damageRing.add(scaledFlushDamage);
        }
    }

    if (auto dr = qobject_cast<QSGDefaultRenderContext*>(state.context)) {
        QRhiResourceUpdateBatch *resourceUpdates = wd->rhi->nextResourceUpdateBatch();
        dr->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates);
    }

    if (shouldCacheBuffer())
        wTextureProvider()->setBuffer(state.buffer);
}

處理完畫面以後,如果需要上屏畫面,就調用 commit 把畫面送到螢幕上。

bool OutputHelper::commit(WBufferRenderer *buffer)
{
    if (output()->offscreen())
        return true;

    if (!buffer || !buffer->currentBuffer()) {
        Q_ASSERT(!this->buffer());
        return WOutputHelper::commit();
    }

    setBuffer(buffer->currentBuffer());

    if (m_lastCommitBuffer == buffer) {
        if (pixman_region32_not_empty(&buffer->damageRing()->handle()->current))
            setDamage(&buffer->damageRing()->handle()->current);
    }

    m_lastCommitBuffer = buffer;

    return WOutputHelper::commit();
}

還會判斷是否有硬體加速(GPU),會優先使用硬體來加速計算過程。

} else {
    state.worldTransform.optimize();

    bool flipY = wd->rhi ? !wd->rhi->isYUpInNDC() : false;
    if (state.renderTarget.mirrorVertically())
        flipY = !flipY;

    if (viewportRect.isValid()) {
        QRect vr = viewportRect;
        if (flipY)
            vr.moveTop(-vr.y() + state.pixelSize.height() - vr.height());
        renderer->setViewportRect(vr);
    } else {
        renderer->setViewportRect(QRect(QPoint(0, 0), state.pixelSize));
    }

    QRectF rect = sourceRect;
    if (!rect.isValid())
        rect = QRectF(QPointF(0, 0), QSizeF(state.pixelSize) / devicePixelRatio);

    const float left = rect.x();
    const float right = rect.x() + rect.width();
    float bottom = rect.y() + rect.height();
    float top = rect.y();

    if (flipY)
        std::swap(top, bottom);

    QMatrix4x4 matrix;
    matrix.ortho(left, right, bottom, top, 1, -1);

    QMatrix4x4 projectionMatrix, projectionMatrixWithNativeNDC;
    projectionMatrix = matrix * state.worldTransform;

    if (wd->rhi && !wd->rhi->isYUpInNDC()) {
        std::swap(top, bottom);

        matrix.setToIdentity();
        matrix.ortho(left, right, bottom, top, 1, -1);
    }
    projectionMatrixWithNativeNDC = matrix * state.worldTransform;

    renderer->setProjectionMatrix(projectionMatrix);
    renderer->setProjectionMatrixWithNativeNDC(projectionMatrixWithNativeNDC);

    auto textureRT = static_cast<QRhiTextureRenderTarget*>(state.sgRenderTarget.rt);
    if (preserveColorContents) {
        textureRT->setFlags(textureRT->flags() | QRhiTextureRenderTarget::PreserveColorContents);
    } else {
        textureRT->setFlags(textureRT->flags() & ~QRhiTextureRenderTarget::PreserveColorContents);
    }
}

Surface 渲染#

在 Treeland 中,為 Surface 創建了 WSurfaceItem,用於表示一個窗口,並創建了 WSurfaceContent 作為 WSurfaceItem 的 delegate。

void WSurfaceItemPrivate::initForDelegate()
{
    Q_Q(WSurfaceItem);

    std::unique_ptr<QQuickItem> newContentContainer;

    if (!delegate) {
        if (getItemContent()) {
            Q_ASSERT(!delegateIsDirty);
            return;
        }

        delegateIsDirty = false;
        auto contentItem = new WSurfaceItemContent(q);
        if (surface)
            contentItem->setSurface(surface);
        contentItem->setCacheLastBuffer(!surfaceFlags.testFlag(WSurfaceItem::DontCacheLastBuffer));
        contentItem->setSmooth(q->smooth());
        contentItem->setLive(!q->flags().testFlag(WSurfaceItem::NonLive));
        QObject::connect(q, &WSurfaceItem::smoothChanged, contentItem, &WSurfaceItemContent::setSmooth);
        newContentContainer.reset(contentItem);
    } else if (delegateIsDirty) {
        auto obj = delegate->createWithInitialProperties({{"surface", QVariant::fromValue(q)}}, qmlContext(q));
        if (!obj) {
            qWarning() << "Failed on create surface item from delegate, error mssage:"
                       << delegate->errorString();
            return;
        }

        delegateIsDirty = false;
        auto contentItem = qobject_cast<QQuickItem*>(obj);
        if (!contentItem)
            qFatal() << "SurfaceItem's delegate must is Item";

        newContentContainer.reset(new QQuickItem(q));
        QQmlEngine::setObjectOwnership(contentItem, QQmlEngine::CppOwnership);
        contentItem->setParent(newContentContainer.get());
        contentItem->setParentItem(newContentContainer.get());
    }

    if (!newContentContainer)
        return;

    newContentContainer->setZ(qreal(WSurfaceItem::ZOrder::ContentItem));

    if (contentContainer) {
        newContentContainer->setPosition(contentContainer->position());
        newContentContainer->setSize(contentContainer->size());
        newContentContainer->setTransformOrigin(contentContainer->transformOrigin());
        newContentContainer->setScale(contentContainer->scale());

        contentContainer->disconnect(q);
        contentContainer->deleteLater();
    }
    contentContainer = newContentContainer.release();
    updateEventItem(false);
    updateBoundingRect();
    if (eventItem)
        updateEventItemGeometry();

    Q_EMIT q->contentItemChanged();
}

之後當 WSurfaceItem 需要更新畫面時,就能調用 updatePaintNode 更新渲染。

QSGNode *WSurfaceItemContent::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
    W_D(WSurfaceItemContent);

    auto tp = wTextureProvider();
    if (d->live || !tp->texture()) {
        auto texture = d->surface ? d->surface->handle()->get_texture() : nullptr;
        if (texture) {
            tp->setTexture(qw_texture::from(texture), d->buffer.get());
        } else {
            tp->setBuffer(d->buffer.get());
        }
    }

    if (!tp->texture() || width() <= 0 || height() <= 0) {
        delete oldNode;
        return nullptr;
    }

    auto node = static_cast<QSGImageNode*>(oldNode);
    if (Q_UNLIKELY(!node)) {
        node = window()->createImageNode();
        node->setOwnsTexture(false);
        QSGNode *fpnode = new WSGRenderFootprintNode(this);
        node->appendChildNode(fpnode);
    }

    auto texture = tp->texture();
    node->setTexture(texture);
    const QRectF textureGeometry = d->bufferSourceBox;
    node->setSourceRect(textureGeometry);
    const QRectF targetGeometry(d->ignoreBufferOffset ? QPointF() : d->bufferOffset, size());
    node->setRect(targetGeometry);
    node->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest);

    return node;
}

而使用 delegate 的目的是為了能讓多個 WSurfaceItem 使用相同的窗口畫面,例如某些場景需要臨時創建一個窗口的分身,窗口切換列表、多任務視圖等。

QSGTextureProvider *WSurfaceItemContent::textureProvider() const
{
    if (QQuickItem::isTextureProvider())
        return QQuickItem::textureProvider();

    return wTextureProvider();
}

WSGTextureProvider *WSurfaceItemContent::wTextureProvider() const
{
    W_DC(WSurfaceItemContent);

    auto w = qobject_cast<WOutputRenderWindow*>(d->window);
    if (!w || !d->sceneGraphRenderContext() || QThread::currentThread() != d->sceneGraphRenderContext()->thread()) {
        qWarning("WQuickCursor::textureProvider: can only be queried on the rendering thread of an WOutputRenderWindow");
        return nullptr;
    }

    if (!d->textureProvider) {
        d->textureProvider = new WSGTextureProvider(w);
        if (d->surface) {
            if (auto texture = d->surface->handle()->get_texture()) {
                d->textureProvider->setTexture(qw_texture::from(texture), d->buffer.get());
            } else {
                d->textureProvider->setBuffer(d->buffer.get());
            }
        }
    }
    return d->textureProvider;
}

Treeland 使用 WQuickTextureProxy 創建窗口的代理顯示,而其中就是獲取 WSurfaceItem 的 textureProvider。

QSGTextureProvider *WQuickTextureProxy::textureProvider() const
{
    if (QQuickItem::isTextureProvider())
        return QQuickItem::textureProvider();

    W_DC(WQuickTextureProxy);
    if (!d->sourceItem)
        return nullptr;

    return d->sourceItem->textureProvider();
}

這樣多個 proxy 就可以顯示同一個窗口的內容,比 QML 的 ShaderEffectSource 效率更高。

結尾#

上述僅僅是 Treeland 實現 Qt 和 wlroots 縫合的一部分流程,實際上對事件的處理就十分複雜,不止鍵盤輸入,還需要處理光標、觸控、觸摸等其他設備。還有光標的繪製也需要區分硬光標和軟光標,渲染畫面時的硬體加速及軟體實現等。

後續準備寫一下光標相關的處理,以及還沒介紹 Treeland 的畫面是怎麼繪製的。

相關文檔#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。