名詞概念#
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
は深層プラットフォームテーマと統合に使用されます。
基本作業フロー#
Treeland
はQQuickWindow
をレンダリングのルートとして使用します。これにより、Treeland
での開発は通常の Qt プログラムの開発と同様に、まずウィンドウを作成し、そのウィンドウ内に Qt ウィジェットを作成し、QEvent を使用してさまざまなイベントを処理します。
では、Treeland はどのようにこれを実現しているのでしょうか?
QQuickWindow
のプライベートクラスは、カスタムQQuickGraphicsDevice
オブジェクトのインターフェースを提供し、QQuickGraphicsDevice
はfromOpenGLContext
およびfromPhyicalDevice
を使用して新しいオブジェクトを作成できます。したがって、Treeland
はQQuickWindow
を継承し、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 にクロスプラットフォームのインターフェース抽象能力を提供します。私たちは独自の 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
のイベント処理では、シートのイベントフィルターを追加で呼び出し、コンポジタが一部のイベントをキャッチできるようにします。たとえば、一部のキーをキャッチしてクライアントに送信しないようにします。
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();
}
QWlrootsScreen
はQPlatformScreen
を継承しており、物理サイズ、デバイスピクセル比、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();
// 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;
// QQuickRenderControlが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 バックエンドのタイプを判断し、vulkan
、gles
などに適応する必要があります。
wlroots
から物理デバイスなどのパラメータを取得し、QQuickGraphicsDevice::fromDeviceObjects
を使用して Qt のQQuickGraphicsDevice
を作成します。
レンダーウィンドウのプライベートクラスはQQuickWindowPrivate
を継承しており、取得したQQuickGraphicsDevice
をQQuickWindowPrivate::setGraphicsDevice
に設定するだけです。
その後、RHI の初期化のためにオフスクリーンレンダリングサーフェスを作成します。
Qt Viewport#
Qt では、コンポーネントを表示またはレンダリングするには、Viewport コンポーネントを使用する必要があります。一般にカメラと呼ばれます。
ビューポート(Viewport)は、観察可能な多角形領域であり、ビューポートの範囲内の画面のみが画面に表示されます。
wlroots
のViewportは、Wayland
表示プロトコルに関連する概念であり、主に画面上の表示領域でのレンダリング出力を定義するために使用されます。これにより、異なる解像度や表示要件に適応するために、レンダリング時に表示内容をスケーリング、クロッピング、またはパンすることができます。
Treeland はWOutputViewport
を使用してビューポート機能を提供し、wlroots
のwlr_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();
// Intel&NvidiaのマルチGPU環境では、wlrootsはIntelカードを使用してすべての出力をレンダリングし、drm_connector_state_update_primary_fbでNvidiaの出力バッファをブリットします。
// この「ブリット」動作は、EGLコンテキストをNvidiaレンダラーにします。したがって、次のフレームの前にQtQuickが常にEGLコンテキストをIntelレンダラーに作成することを保証するために、ここで現在のOpenGLコンテキストを完了する必要があります。
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());
// スワップチェーンを構成
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: wlr_surfaceのスキャンアウトバッファをサポート
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();
// もしかしたら他のWOutputViewportの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の前に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
を作成し、ウィンドウを表すためにWSurfaceContent
をデリゲートとして作成しました。
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 の画面がどのように描画されるかについても書く予定です。