名詞概念#
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
用於窗口系統集成,而 QPlatformTheme
和 QStyleHint
則用於深層次的平台主題和集成。
基本工作流程#
Treeland
使用 QQuickWindow
作為渲染的根,這樣在 Treeland
裡開發時,就如同開發一個普通 Qt 程序一樣,先創建一個 Window,在 Window 內創建 Qt 控件,使用 QEvent 處理各種事件。
那麼 Treeland 是如何實現這件事的呢?
QQuickWindow
的私有類提供了自定義 QQuickGraphicsDevice
對象的接口,而 QQuickGraphicsDevice
可以使用 fromOpenGLContext
和 fromPhyicalDevice
創建新的對象,那麼 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 的類型,需要適配 vulkan
、gles
等。
從 wlroots
獲取物理設備等參數,使用 QQuickGraphicsDevice::fromDeviceObjects
創建 Qt 的 QQuickGraphicsDevice
。
render window 的私有類是繼承自 QQuickWindowPrivate
,只需要將獲取到的 QQuickGraphicsDevice
設置給 QQuickWindowPrivate::setGraphicsDevice
即可。
之後創建一個離屏渲染表面,用於 RHI 的初始化。
Qt Viewport#
在 Qt 中,想要查看或者渲染一個組件,需要使用 Viewport 組件,俗稱照相機。
視口(Viewport)是一個可觀察的多邊形區域,只有 Viewport 範圍內的畫面才能顯示到螢幕上。
wlroots
中的 Viewport 是一個與 Wayland
顯示協議相關的概念,主要用於定義渲染輸出在螢幕上的顯示區域。它允許在渲染時對顯示內容進行縮放、裁剪或平移,以適應不同的解析度和顯示需求。
Treeland 使用 WOutputViewport
提供 Viewport 功能,使用 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
提供了 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 的畫面是怎麼繪製的。