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#

特定の作業シーンに割り当てられたすべてのハードウェアデバイスで構成されています。少なくとも 1 つのグラフィックデバイスを含み、通常はキーボードとマウスも含まれます。さらに、カメラやサウンドカードなどのデバイスが含まれる場合もあります。シートはシート名で識別され、これは短い文字列(64 文字以内)で、「seat」の 4 文字で始まり、少なくとも 1 つの 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はウィンドウシステム統合に使用され、QPlatformThemeおよびQStyleHintは深層プラットフォームテーマおよび統合に使用されます。

基本作業フロー#

TreelandQQuickWindowをレンダリングのルートとして使用します。これにより、Treeland内で開発する際は、通常の Qt プログラムを開発するのと同様に、最初にウィンドウを作成し、そのウィンドウ内に Qt ウィジェットを作成し、QEvent を使用してさまざまなイベントを処理します。

では、Treeland はこの作業をどのように実現しているのでしょうか?

QQuickWindowのプライベートクラスは、カスタムQQuickGraphicsDeviceオブジェクトのインターフェースを提供し、QQuickGraphicsDevicefromOpenGLContextおよびfromPhyicalDeviceを使用して新しいオブジェクトを作成できます。したがって、TreelandQQuickWindowを継承し、wlrootsから OpenGL コンテキストと物理デバイスを取得するだけで、Qt QuickWindow のレンダリングをwlrootsに接続できます。

wlrootsのレンダリングコンテキストと Qt のレンダリングコンテキストを組み合わせることで、wlrootsがレンダリングしたグラフィック結果を Qt アプリケーションのレンダリングプロセスに埋め込むことができ、物理デバイス(phdev)、論理デバイス(dev)、およびキュー族(queue_family)など、wlrootsが提供するグラフィックリソースとデバイスオブジェクトを直接使用して、不要なコンテキストスイッチやリソースコピーを減らすことができます。これにより、Qt はwlrootsが提供するレンダリング能力を利用しながら、Qt のレンダリングフレームワークと API を引き続き使用できます。

その後、Qt QPA 内で画面情報と入力情報を Qt 内部オブジェクトに変換し、Qt 自身のイベントループなどのメカニズムを利用して処理を続けます。

Qt QPA#

QPA は Qt にクロスプラットフォームのインターフェース抽象能力を提供し、Qt プログラムに新しい機能を提供するために独自の QPA プラグインを提供できます。たとえば、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のイベント処理では、シートのイベントフィルターを追加で呼び出し、コンポジタが一部のイベントをキャッチできるようにします。たとえば、一部のキーをキャッチしてクライアントに送信しないようにします。

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)) {
                // プライマリポインティングデバイスが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)) {
                // プライマリキーボードデバイスがWInputDeviceであることを確認します
                auto pd = const_cast<QInputDevice*>(primaryQtDevice);
                pd->setParent(nullptr);
                delete pd;
            }
            Q_ASSERT(WInputDevice::from(QInputDevice::primaryKeyboard()));
        }
    }

    return qtdev;
}

クライアントイベント#

Treeland には、ユーザーがウィンドウをクリックしたときに、コンポジタがクライアントにどの座標がクリックされたかを通知する必要があるという別のイベントがあります。また、キーボードで入力する際には、クライアントに入力内容を通知する必要があります。

まず、Treeland はウィンドウをアクティブウィンドウとしてマークし、シートに設定します。これにより、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();
}

QWlrootsScreenQPlatformScreenを継承しており、物理サイズ、デバイスピクセル比、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 バックエンドのタイプを判断し、vulkanglesなどに適応する必要があります。

wlrootsから物理デバイスなどのパラメータを取得し、QQuickGraphicsDevice::fromDeviceObjectsを使用して Qt のQQuickGraphicsDeviceを作成します。

レンダーウィンドウのプライベートクラスはQQuickWindowPrivateを継承しており、取得したQQuickGraphicsDeviceQQuickWindowPrivate::setGraphicsDeviceに設定するだけで済みます。

その後、RHI の初期化のためにオフスクリーンレンダリングサーフェスを作成します。

Qt Viewport#

Qt では、コンポーネントを表示またはレンダリングするには、Viewport コンポーネントを使用する必要があります。一般的にカメラと呼ばれます。

ビューポート(Viewport)は、観察可能な多角形領域であり、ビューポートの範囲内の画面のみが画面に表示されます。

wlrootsViewportは、Wayland 表示プロトコルに関連する概念で、レンダリング出力が画面に表示される領域を定義するために主に使用されます。これにより、レンダリング時に表示内容をスケーリング、クロッピング、またはパンすることができ、異なる解像度や表示要件に適応できます。

Treeland はWOutputViewportを使用してビューポート機能を提供し、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は、ビューポートに必要なすべてのパラメータ、変換行列、ソース幾何サイズ、ターゲット幾何サイズなどの情報を提供します。

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) {
            // ソフトウェアレンダラーはビューポート矩形をサポートしていないため、
            // 変換を使用してシミュレーションします。
            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: QSGSoftwareRendererに設定する必要がありますが、特定の行列をサポートしていません。
            // 変換が変更されると、完全に再描画されます。
            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: QRhiレンダラーからダメージ領域を取得します
            m_damageRing.add_whole();
            // ###: Qtのバグかもしれません? QRhi::endOffscreenFrameを実行する前に、
            // 同じQSGRendererを複数の描画に使用する可能性があります。これにより、
            // RhiGlesバックエンドを使用しているときに、異なるQSGRhiRenderTargetインスタンスに対して
            // 同じコンテンツがレンダリングされる可能性があります。さらに、現在の描画の結果が
            // 次のsourceIndexを描画するときに必要になる可能性があるため、ここでRHI(Rendering Hardware Interface)
            // がこの描画の結果を完了させる必要があります。これにより、現在の描画結果が
            // 使用可能になります。
            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: 前のレンダーターゲットを使用しないでください。QQuickRenderControl::renderの前に
                    // qw_damage_ringのQtQuickのダメージ領域を取得し、現在のレンダーターゲットのダメージ領域を強制的に再描画するために
                    // QSGAbstractSoftwareRendererにダーティ領域を追加できます。
                    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を作成し、ウィンドウを表すためにWSurfaceContentWSurfaceItemのデリゲートとして作成しました。

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;
}

デリゲートを使用する目的は、複数の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();
}

これにより、複数のプロキシが同じウィンドウの内容を表示でき、QML のShaderEffectSourceよりも効率的です。

結尾#

上記は、Treeland が Qt と wlroots を統合するプロセスの一部に過ぎません。実際、イベントの処理は非常に複雑で、キーボード入力だけでなく、カーソル、タッチ、タッチなどの他のデバイスも処理する必要があります。また、カーソルの描画もハードカーソルとソフトカーソルを区別する必要があり、レンダリング画面時のハードウェアアクセラレーションとソフトウェア実装なども考慮する必要があります。

今後、カーソル関連の処理や、まだ紹介していない Treeland の画面がどのように描画されるかについても書く予定です。

関連文書#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。