[ Content contribution] 带你瞅瞅窗口管理器(KWin)是如何渲染一副画面的
Tofloor
poster avatar
justforlxz
deepin
2025-12-26 09:15
Author

好久不见,甚是想念,给大家来点稍微没那么湿的干货,有些地方讲的可能太跳跃了,希望大家可以结合实际代码走一遍。

照例给自己打个广告tail

我是 Deepin 的开发者竹子,现在负责维护 deepin fork 的 kwin,偶尔兼职 DDE 移植和 Treeland 新项目研发。

Github: https://github.com/justforlxz

Blog: https://blog.mkacg.com


本文尝试从源码调用链的角度,梳理一次 KWin 完整的渲染流程。

需要提前说明的是,文章并不会对出现的代码做逐行语义解释,也不会深入讨论具体的渲染算法、OpenGL 状态细节或特效本身的视觉实现。这里关注的核心问题只有一个:一次画面是如何在 KWin 内部被触发、调度并最终提交的。

在阅读 KWin 源码时,很容易被大量接口类、虚函数和跨子系统调用打断思路,例如 RenderLoop、Compositor、Scene、EffectsHandler、ItemRenderer 等模块彼此交织。如果不先把调用顺序和职责边界理清楚,单独看某个函数往往很难判断它处在渲染管线的哪个阶段。

因此,下面的分析会刻意保持在调用关系和阶段划分这一层级:

  • 渲染请求是由谁、在什么时机发起的
  • 合成器在一次 composite 中拆分了哪些阶段
  • 场景、特效和渲染后端分别介入在哪一步
  • 特效链是如何通过迭代器串联执行的

代码片段的选取仅用于定位关键路径,其它分支和边缘逻辑会被省略或忽略。

合成器执行

KWin 的渲染是以物理屏幕为单位执行的,所以首先在添加屏幕时,会添加一个 RenderLayer 作为渲染层,当需要为该屏幕更新画面时,调用对应的 RenderLayer 对象就可以了。

void Compositor::addOutput(Output *output)
{
    Q_ASSERT(kwinApp()->operationMode() != Application::OperationModeX11);

    auto workspaceLayer = new RenderLayer(output->renderLoop());
    workspaceLayer->setDelegate(std::make_unique(m_scene.get(), output));
    workspaceLayer->setGeometry(output->rect());

    addSuperLayer(workspaceLayer);
}
void Compositor::addSuperLayer(RenderLayer *layer)
{
    m_superlayers.insert(layer->loop(), layer);
    connect(layer->loop(), &RenderLoop::frameRequested, this, &Compositor::handleFrameRequested);
}
void Compositor::handleFrameRequested(RenderLoop *renderLoop)
{
    composite(renderLoop);
}

从上述代码可以看出,添加一个屏幕后,会为 RenderLayer 绑定信号槽,在槽函数中执行 composite

现在让我们关注一下 frameRequested 是从哪里发出的。

RenderLoopPrivate::RenderLoopPrivate(RenderLoop *q)
    : q(q)
{
    compositeTimer.setSingleShot(true);
    QObject::connect(&compositeTimer, &QTimer::timeout, q, [this]() {
        dispatch();
    });
}
void RenderLoopPrivate::scheduleRepaint()
{
    // 省略代码

    if (presentMode == SyncMode::Async || presentMode == SyncMode::AdaptiveAsync) {
        compositeTimer.start(0);
    } else {
        const std::chrono::nanoseconds waitInterval = nextRenderTimestamp - currentTime;
        compositeTimer.start(std::chrono::duration_cast(waitInterval));
    }
}
void RenderLoopPrivate::dispatch()
{
    // On X11, we want to ignore repaints that are scheduled by windows right before
    // the Compositor starts repainting.
    pendingRepaint = true;

    Q_EMIT q->frameRequested(q);

    // The Compositor may decide to not repaint when the frameRequested() signal is
    // emitted, in which case the pending repaint flag has to be reset manually.
    pendingRepaint = false;
}

scheduleRepaint 表示该输出存在未呈现的视觉变化,其来源可能是 Wayland surface damage、动画、特效、光标或屏幕输出状态变化。

RenderLoop 并不关心变化来自哪里,而是根据 presentMode 决定何时通过 frameRequested 触发一次合成。

至此,我们已经知道了 KWin 是从何时产生一次渲染操作的,回到最开始的位置,开始执行一次 composite 时,里面又在做哪些事情。

void Compositor::composite(RenderLoop *renderLoop)
{
    if (m_backend->checkGraphicsReset()) {
        qCDebug(KWIN_CORE) << "Graphics reset occurred...";
        reinitialize();
        return;
    }

    Output *output = findOutput(renderLoop);
    OutputLayer *primaryLayer = m_backend->primaryLayer(output);
    fTraceDuration("Paint (", output->name(), ")");

    RenderLayer *superLayer = m_superlayers[renderLoop];
    prePaintPass(superLayer); // << 别忘了这里
    superLayer->setOutputLayer(primaryLayer);

    SurfaceItem *scanoutCandidate = superLayer->delegate()->scanoutCandidate();
    renderLoop->setFullscreenSurface(scanoutCandidate);
    output->setContentType(scanoutCandidate ? scanoutCandidate->contentType() : ContentType::None);

    renderLoop->beginFrame();
    bool directScanout = false;
    if (scanoutCandidate) {
        const auto sublayers = superLayer->sublayers();
        const bool scanoutPossible = std::none_of(sublayers.begin(), sublayers.end(), [](RenderLayer *sublayer) {
            return sublayer->isVisible();
        });
        if (scanoutPossible && !output->directScanoutInhibited()) {
            directScanout = primaryLayer->scanout(scanoutCandidate);
        }
    }

    if (!directScanout) {
        QRegion surfaceDamage = primaryLayer->repaints();
        primaryLayer->resetRepaints();
        preparePaintPass(superLayer, &surfaceDamage);

        if (auto beginInfo = primaryLayer->beginFrame()) {
            auto &[renderTarget, repaint] = beginInfo.value();
            renderTarget.setDevicePixelRatio(output->scale());

            const QRegion bufferDamage = surfaceDamage.united(repaint).intersected(superLayer->rect());
            primaryLayer->aboutToStartPainting(bufferDamage);

            // TODO: fixme
            if (waylandServer() && workspace()->outputs().size() > 1) {
                surfaceDamage = bufferDamage;
            }
            paintPass(superLayer, &renderTarget, bufferDamage);
            primaryLayer->endFrame(bufferDamage, surfaceDamage);
        }
    }

    postPaintPass(superLayer);
    renderLoop->endFrame();

    if (workspace()->getDebugPixmapState() & 0x01) {
        workspace()->setDebugPixmaState(workspace()->getDebugPixmapState() ^ 0x01);
        workspace()->getDebugPixmapPtr()->saveCompositePixmap();
    }

    m_backend->present(output);

    // TODO: Put it inside the cursor layer once the cursor layer can be backed by a real output layer.
    if (waylandServer()) {
        const std::chrono::milliseconds frameTime =
            std::chrono::duration_cast(output->renderLoop()->lastPresentationTimestamp());

        if (!Cursors::self()->isCursorHidden()) {
            Cursor *cursor = Cursors::self()->currentCursor();
            if (cursor->geometry().intersects(output->geometry())) {
                cursor->markAsRendered(frameTime);
            }
        }
    }
}

KWin 将绘制画面的操作分为了四个阶段:prePaintPasspreparePaintPasspaintPassPostPaintPass

prePaintPass —— 状态预处理阶段

职责

  • 通知各个 RenderLayerDelegateSceneEffect:一帧即将开始
  • 更新与下一帧相关的逻辑状态
    • 动画推进
    • 特效内部状态更新
    • 窗口属性变化(透明度、变换、可见性等)

阶段语义

为即将到来的这一帧准备状态,但不决定画哪里,也不画任何东西。

void Compositor::prePaintPass(RenderLayer *layer)
{
    layer->delegate()->prePaint();
    const auto sublayers = layer->sublayers();
    for (RenderLayer *sublayer : sublayers) {
        prePaintPass(sublayer);
    }
}

preparePaintPass —— 重绘区域汇总阶段

职责

  • 汇总各个 RenderLayer 及其 delegate 上报的 repaint 区域
  • 将局部 repaint 区域统一映射到全局坐标系
  • 生成本次 composite 的最终 repaint / buffer damage 区域

阶段语义

决定这一帧“哪些区域需要被重画”。

void Compositor::preparePaintPass(RenderLayer *layer, QRegion *repaint)
{
    // TODO: Cull opaque region.
    *repaint += layer->mapToGlobal(layer->repaints() + layer->delegate()->repaints());
    layer->resetRepaints();
    const auto sublayers = layer->sublayers();
    for (RenderLayer *sublayer : sublayers) {
        if (sublayer->isVisible()) {
            preparePaintPass(sublayer, repaint);
        }
    }
}

paintPass —— 实际绘制阶段

职责

  • 驱动 Scene → Effects → Renderer 的完整绘制链路
  • 执行屏幕级与窗口级的绘制流程:
    • 背景绘制与屏幕级特效
    • 窗口遍历、窗口级特效链
  • 将绘制指令最终提交给具体渲染后端(如 OpenGL)

阶段语义

真正把这一帧画出来的阶段。

void Compositor::paintPass(RenderLayer *layer, RenderTarget *target, const QRegion ®ion)
{
    layer->delegate()->paint(target, region);

    const auto sublayers = layer->sublayers();
    for (RenderLayer *sublayer : sublayers) {
        if (sublayer->isVisible()) {
            paintPass(sublayer, target, region);
        }
    }
}

postPaintPass —— 帧结束清理阶段

职责

  • 通知各层和特效:本帧绘制已完成
  • 清理本帧使用的一次性状态与临时数据
  • 恢复到可安全开始下一帧的稳定状态

阶段语义

帧结束后的收尾与清理,为下一帧做准备。

void Compositor::postPaintPass(RenderLayer *layer)
{
    layer->delegate()->postPaint();
    const auto sublayers = layer->sublayers();
    for (RenderLayer *sublayer : sublayers) {
        postPaintPass(sublayer);
    }
}

在四个函数中,都支持递归处理 sublayer,这些代码都可以先无视掉。

重点关注 paintPass 中调用的 layer->delegate()->paint(target, region);,在这里开始,KWin 才是真正开始着手处理渲染。

在 KWin 中,所有的子系统都是由接口类定义的,这使得 KWin 可以随意更换子系统的实现,例如合成器可以换成 XRander、OpenGL,送显有窗口化和 DRM。

paint 函数一个接口函数,layer、delegate、scene 都是渲染子系统定义的多个接口类,在 scene 中定义了交互方式,之后就由不同的渲染后端提供对应的实现。

渲染子系统里定义的接口类: RenderLayerDelegate ItemRenderer Scene Item

  • RenderLayerDelegate 是 RenderLayer 的代理,负责调用 scene 的接口
  • Scene 是场景调度,负责连通特效子系统和渲染子系统
  • ItemRenderer 是渲染后端,负责渲染一个 Item
  • Item 是一个渲染对象,通常理解为窗口
void SceneDelegate::paint(RenderTarget *renderTarget, const QRegion ®ion)
{
    m_scene->paint(renderTarget, region.translated(viewport().topLeft()));
}

delegate 里最终调用的是 WorkspaceScene::paintWorkspaceScene 就是接口类 Scene 的实现。

void WorkspaceScene::paint(RenderTarget *renderTarget, const QRegion ®ion)
{
    m_renderer->beginFrame(renderTarget);

    ScreenPaintData data(m_renderer->renderTargetProjectionMatrix(), EffectScreenImpl::get(painted_screen));
    effects->paintScreen(m_paintContext.mask, region, data);
    m_paintScreenCount = 0;
    Q_EMIT frameRendered();

    m_renderer->endFrame();
}

ScreenPaintData 里存储的是绘制相关的数据,里面其实只有屏幕和矩阵的信息。

class ScreenPaintData::Private
{
public:
    QMatrix4x4 projectionMatrix;
    EffectScreen *screen = nullptr;
};

WorkspaceScene::paint 中,在 beginFrameendFrame之间,effects->paintScreen() 开始一幅画面的处理了。

从名字上也能看得出,这里是特效子系统绘制屏幕的函数,和渲染子系统一样,特效子系统也是有好几个接口类规定了交互方式。

本次我们只关注流程,函数是如何调用的,所以就对不重要的地方先略过了。

beginFrameendFrame 调用的都是 m_renderer,根据上述的介绍,渲染器就要看此时配置使用的哪个,目前应该以 OpenGL 渲染方式为主。

void ItemRendererOpenGL::beginFrame(RenderTarget *renderTarget)
{
    GLFramebuffer *fbo = std::get(renderTarget->nativeHandle());
    GLFramebuffer::pushFramebuffer(fbo);

    GLVertexBuffer::streamingBuffer()->beginFrame();
}
void ItemRendererOpenGL::endFrame()
{
    GLVertexBuffer::streamingBuffer()->endOfFrame();
    GLFramebuffer::popFramebuffer();
}

这两个函数的代码量不多,就是做些绘制前和绘制后的工作,让我们重点开始看特效的调用,特效子系统的实现是在 EffectsHandlerImpl 类中,在该类中,有这些函数是我们需要关注的。

class KWIN_EXPORT EffectsHandlerImpl : public EffectsHandlerEx
{
    Q_OBJECT
public:
    void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override;
    void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override;
    void postPaintScreen() override;
    void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override;
    void paintWindow(EffectWindow *w, int mask, const QRegion ®ion, WindowPaintData &data) override;
    void postPaintWindow(EffectWindow *w) override;
    void drawWindow(EffectWindow *w, int mask, const QRegion ®ion, WindowPaintData &data) override;
    void renderWindow(EffectWindow *w, int mask, const QRegion ®ion, WindowPaintData &data) override;
};

从函数的名字上可以看出,KWin 将很多内容都拆分为三个阶段:prepaintpost,然后将具体的工作放在 paint 中,但是当我们真的打开 paintScreen 时,我们就会傻眼了,这都写的啥、啥、啥(自动播放王宝强语音)。

void EffectsHandlerImpl::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data)
{
    if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) {
        (*m_currentPaintScreenIterator++)->paintScreen(mask, region, data);
        --m_currentPaintScreenIterator;
    } else {
        m_scene->finalPaintScreen(mask, region, data);
    }
}

从现在开始,会开始烧脑,特别是在编辑器里点一下函数发现是虚函数,然后又要跳回去重新找实现,然后发现又回到了原地时。

根据代码可以看出,这里是在检查当前的迭代器是不是激活特效的最后一个,如果不是,就调用下一个,然后再减回来。如果已经是最后一个,则会调用 finalPaintScreen

至少我们能先看懂这个最终绘制屏幕函数,让我们先看一下这个迭代器里有什么吧。

// start another painting pass
void EffectsHandlerImpl::startPaint()
{
    m_activeEffects.clear();
    m_activeEffects.reserve(loaded_effects.count());
    for (QVector::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) {
        if (it->second->isActive()) {
            m_activeEffects << it->second;
        }
    }
    m_currentDrawWindowIterator = m_activeEffects.constBegin();
    m_currentPaintWindowIterator = m_activeEffects.constBegin();
    m_currentPaintScreenIterator = m_activeEffects.constBegin();
}

看起来这里就是所有激活的特效插件,那么 paintScreen 里应该就是调用一个特效插件的 paintScreen。来看一下代码。

void Effect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data)
{
    effects->paintScreen(mask, region, data);
}

这个 effects 是 EffectsHandler,而 EffectsHandlerImpl 的基类就是 EffectsHandler...

噫,这什么鬼代码,怎么自己又调回来自己了。

这时候就要回想一些很重要的事情了,KWin 是 C++ 开发的,C++ 是面向对象的语言,而面向对象编程有三个特点:封装 继承 多态。既然这里代码这么诡异,那就应该看一下 paintScreen 到底是个啥了!

其实在上面我已经贴出来了重要信息,不知道各位有没有发现,paintScreen 竟然有一个 override 的小尾巴,那 Effect::paintScreen 也是虚函数吗?

class KWINEFFECTS_EXPORT Effect : public QObject
{
    Q_OBJECT
public:
    // 省略
    virtual void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data);
    // 省略
};

果然,这个函数也是虚的!

这也符合最开始的想法,迭代器里执行的函数确实是特效插件的函数,让我们打开一个特效插件,看一下里面的内容。

void MultitaskViewEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data)
{
    // 省略
    effects->paintScreen(mask, region, data);
    // 省略
}

果然,在每个特效插件里,都主动调用了一次 effects->paintScreen,从而回到了最开始看到的奇怪迭代器调用。

现在让我们梳理一遍流程:

  1. delegate 调用 scene 的 paintScreen
  2. scene 的派生类 WorkspaceScene 调用了特效的 paintScreen
  3. EffectsHandlerImpl 判断当前迭代器是不是末尾
  4. 如果不是末尾,则在自增前调用特效的 paintScreen
  5. 特效在 paintScreen 函数里调用 EffectsHandlerImpl 的 paintScreen
  6. 重复 4 和 5,直到特效执行到最后一个,开始逐层返回
  7. 迭代器自减,直到所有特效执行完毕
  8. 执行 finalPaintScreen

此处省略在分析代码时的心酸

既然已经执行到了 finalPaintScreen,那就看一下 finalPaintScreen 的实现吧。

// the function that'll be eventually called by paintScreen() above
void WorkspaceScene::finalPaintScreen(int mask, const QRegion ®ion, ScreenPaintData &data)
{
    m_paintScreenCount++;
    if (mask & (PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS)) {
        paintGenericScreen(mask, data);
    } else {
        paintSimpleScreen(mask, region);
    }
}

无论是 paintGenericScreen 还是 paintSimpleScreen,基本结构都是类似的,遍历窗口列表,为每个窗口执行 paint

// The generic painting code that can handle even transformations.
// It simply paints bottom-to-top.
void WorkspaceScene::paintGenericScreen(int, const ScreenPaintData &)
{
    if (m_paintContext.mask & PAINT_SCREEN_BACKGROUND_FIRST) {
        if (m_paintScreenCount == 1) {
            m_renderer->renderBackground(infiniteRegion());
        }
    } else {
        m_renderer->renderBackground(infiniteRegion());
    }

    for (const Phase2Data &paintData : std::as_const(m_paintContext.phase2Data)) {
        paintWindow(paintData.item, paintData.mask, paintData.region);
    }
}

// The optimized case without any transformations at all.
// It can paint only the requested region and can use clipping
// to reduce painting and improve performance.
void WorkspaceScene::paintSimpleScreen(int, const QRegion ®ion)
{
    // This is the occlusion culling pass
    QRegion visible = region;
    for (int i = m_paintContext.phase2Data.size() - 1; i >= 0; --i) {
        Phase2Data *data = &m_paintContext.phase2Data[i];
        data->region = visible;

        if (!(data->mask & PAINT_WINDOW_TRANSFORMED)) {
            data->region &= data->item->mapToGlobal(data->item->boundingRect()).toAlignedRect();

            if (!(data->mask & PAINT_WINDOW_TRANSLUCENT)) {
                visible -= data->opaque;
            }
        }
    }

    if (!visible.isEmpty()) {
        m_renderer->renderBackground(visible);
    }

    for (const Phase2Data &paintData : std::as_const(m_paintContext.phase2Data)) {
        paintWindow(paintData.item, paintData.mask, paintData.region);
    }

    if (m_dndIcon) {
        const QRegion repaint = region & m_dndIcon->mapToGlobal(m_dndIcon->boundingRect()).toRect();
        if (!repaint.isEmpty()) {
            m_renderer->renderItem(m_dndIcon.get(), 0, repaint, WindowPaintData(m_renderer->renderTargetProjectionMatrix()));
        }
    }
}

有一个需要注意的是,在 paintSimpleScreen 中存在一个遮挡剔除的功能。

paintWindow 就比较简答了,和 screen 一个套路。

void WorkspaceScene::paintWindow(WindowItem *item, int mask, const QRegion ®ion)
{
    if (region.isEmpty()) { // completely clipped
        return;
    }

    // 省略代码

    WindowPaintData data(m_renderer->renderTargetProjectionMatrix());
    effects->paintWindow(item->window()->effectWindow(), mask, region, data);
}
void EffectsHandlerImpl::paintWindow(EffectWindow *w, int mask, const QRegion ®ion, WindowPaintData &data)
{
    if (m_currentPaintWindowIterator != m_activeEffects.constEnd()) {
        (*m_currentPaintWindowIterator++)->paintWindow(w, mask, region, data);
        --m_currentPaintWindowIterator;
    } else {
        m_scene->finalPaintWindow(static_cast(w), mask, region, data);
    }
}

但是需要留意的是,在开发插件时,如果我们想要直接替换画面,那应该先调用 effects->paintWindow,将其他插件的画面都绘制上去,再绘制自己的。

举个例子,窗口模糊插件是先在窗口位置上绘制一块模糊背景,再调用 effectc->paintWindow 绘制窗口本体画面,而水印插件则是先绘制窗口本体画面,再绘制水印内容。

这是一个非常关键的设计约束:

  • 如果不调用,下游特效和最终绘制将被完全截断
  • 调用的位置,决定了绘制顺序

在 effects 的 paintWindow 的最后一步里,又调用回了 scene 的 finalPaintWindow,然后调用 drawWindow,再经过同样的一套流程,最终执行到了 scene 的 finalDrawWindow 函数。

// will be eventually called from drawWindow()
void WorkspaceScene::finalDrawWindow(EffectWindowImpl *w, int mask, const QRegion ®ion, WindowPaintData &data)
{
    // 省略代码

    m_renderer->renderItem(w->windowItem(), mask, region, data);
}

在这个函数里,调用到了渲染的 renderItem 函数,看一眼 OpenGL 渲染后端的代码。

void ItemRendererOpenGL::renderItem(Item *item, int mask, const QRegion ®ion, const WindowPaintData &data)
{
    if (region.isEmpty()) {
        return;
    }

    RenderContext renderContext{
        .clip = region,
        .hardwareClipping = region != infiniteRegion() && ((mask & Scene::PAINT_WINDOW_TRANSFORMED) || (mask & Scene::PAINT_SCREEN_TRANSFORMED)),
        .renderTargetScale = data.renderTargetScale().value_or(renderTargetScale()),
    };

    renderContext.transformStack.push(QMatrix4x4());
    renderContext.opacityStack.push(data.opacity());

    item->setTransform(data.toMatrix(renderTargetScale()));

    createRenderNode(item, &renderContext);

    int totalVertexCount = 0;
    for (const RenderNode &node : std::as_const(renderContext.renderNodes)) {
        totalVertexCount += node.geometry.count();
    }
    if (totalVertexCount == 0) {
        return;
    }

    const size_t size = totalVertexCount * sizeof(GLVertex2D);

    ShaderTraits shaderTraits = ShaderTrait::MapTexture;

    if (data.brightness() != 1.0) {
        shaderTraits |= ShaderTrait::Modulate;
    }
    if (data.saturation() != 1.0) {
        shaderTraits |= ShaderTrait::AdjustSaturation;
    }

    GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer();
    vbo->reset();
    vbo->setAttribLayout(GLVertexBuffer::GLVertex2DLayout, 2, sizeof(GLVertex2D));

    GLVertex2D *map = (GLVertex2D *)vbo->map(size);

    for (int i = 0, v = 0; i < renderContext.renderNodes.count(); i++) {
        RenderNode &renderNode = renderContext.renderNodes[i];
        if (renderNode.geometry.isEmpty() || !renderNode.texture) {
            continue;
        }

        if (renderNode.opacity != 1.0) {
            shaderTraits |= ShaderTrait::Modulate;
        }

        if (renderNode.texture->target() == GL_TEXTURE_EXTERNAL_OES) {
            shaderTraits |= ShaderTrait::MapExternalTexture;
        }

        renderNode.firstVertex = v;
        renderNode.vertexCount = renderNode.geometry.count();

        renderNode.geometry.postProcessTextureCoordinates(renderNode.texture->matrix(renderNode.coordinateType));

        renderNode.geometry.copy(&map[v], renderNode.geometry.count());
        v += renderNode.geometry.count();
    }

    vbo->unmap();
    vbo->bindArrays();

    GLShader *shader = data.shader;
    if (!shader) {
        shader = ShaderManager::instance()->pushShader(shaderTraits);
    }

    shader->setUniform(GLShader::Saturation, data.saturation());

    if (renderContext.hardwareClipping) {
        glEnable(GL_SCISSOR_TEST);
    }

    // Make sure the blend function is set up correctly in case we will be doing blending
    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

    float opacity = -1.0;

    // The scissor region must be in the render target local coordinate system.
    QRegion scissorRegion = infiniteRegion();
    if (renderContext.hardwareClipping) {
        scissorRegion = mapToRenderTarget(region);
    }

    const QMatrix4x4 projectionMatrix = data.projectionMatrix();
    for (int i = 0; i < renderContext.renderNodes.count(); i++) {
        const RenderNode &renderNode = renderContext.renderNodes[i];
        if (renderNode.vertexCount == 0) {
            continue;
        }

        setBlendEnabled(renderNode.hasAlpha || renderNode.opacity < 1.0);

        shader->setUniform(GLShader::ModelViewProjectionMatrix, projectionMatrix * renderNode.transformMatrix);
        if (opacity != renderNode.opacity) {
            shader->setUniform(GLShader::ModulationConstant,
                               modulate(renderNode.opacity, data.brightness()));
            opacity = renderNode.opacity;
        }

        renderNode.texture->setFilter(GL_LINEAR);
        renderNode.texture->setWrapMode(GL_CLAMP_TO_EDGE);
        renderNode.texture->bind();

        vbo->draw(scissorRegion, GL_TRIANGLES, renderNode.firstVertex,
                  renderNode.vertexCount, renderContext.hardwareClipping);
    }

    vbo->unbindArrays();

    setBlendEnabled(false);

    if (!data.shader) {
        ShaderManager::instance()->popShader();
    }

    if (renderContext.hardwareClipping) {
        glDisable(GL_SCISSOR_TEST);
    }
}

代码略长,大概内容就是绘制窗口数据,设置一些参数。

renderItem() 是一个很好的分界点:

在此之前,所有数据仍然是 逻辑对象 + 渲染参数,从这里开始,才进入 顶点、纹理、Shader、Draw Call。

一个重要但容易忽略的事实是:

特效系统并不直接操作 OpenGL,也不关心具体的绘制 API

它能做的事情被严格限制在:

  • 修改 WindowPaintData
  • 改变 Item 的变换、透明度、裁剪区域
  • 决定是否继续向下传递绘制

真正的几何拆解、VBO 填充、Shader 选择,全部发生在渲染后端内部。

到这里,其实已经可以给整个流程下一个阶段性结论了:

KWin 的渲染管线,本质上是一条由 RenderLoop 驱动、以 Scene 为中枢、通过 Effects 进行可插拔加工,最终落到具体渲染后端的分层流水线

梳理一遍过程:

  1. 从 finalPaintScreen 遍历所有窗口,执行 paintWindow
  2. 执行遮挡剔除,调用 prePaintWindow 设置窗口渲染参数
  3. 完成特效链的加工
  4. 调用渲染子系统的 renderItem 将窗口绘制到屏幕上

本次我们只进行代码的梳理,并未涉及底层的内容,窗口的画面是如何经过特效链加工的?怎么绘制到屏幕上的?WindowPaintData 又是什么?这些都没有涉及。

通过对代码调用链的梳理,可以看出 KWin 的渲染流程并不是一条线性的“绘制函数调用序列”,而是一套由时序驱动、阶段划分明确、子系统职责严格隔离的渲染管线。

从触发机制上看,渲染并非由窗口事件直接发起,而是由 RenderLoop 根据当前的呈现模式(VSync、Async、Adaptive)调度,在合适的时间点通过 frameRequested 信号进入合成器。这一设计将何时渲染与渲染什么彻底解耦。

在合成器内部,一次 composite() 被拆分为 prePaint、preparePaint、paint、postPaint 四个阶段。这种拆分并非形式上的分层,而是明确区分了:

  • 状态收集与预处理
  • 重绘区域与可见性计算
  • 实际绘制行为
  • 帧结束后的清理与收尾

进一步向下,Scene 作为渲染流程的中枢,将“屏幕级”与“窗口级”绘制分离处理。屏幕级流程负责背景、遍历顺序以及全局遮挡策略,而窗口级流程则以 WindowItem 为最小单位,逐一执行特效链和最终绘制。这种分离使得复杂特效只需关注自身作用的层级,而不必感知整个屏幕状态。

特效子系统的实现是整个流程中最具辨识度的部分。KWin 并未采用传统的“前后回调”或“事件分发”模型,而是通过一个显式维护的迭代器递归调用链,将所有激活的特效串联成一条严格有序的执行路径。特效插件必须主动调用 effects->paintScreen / paintWindow 才能将控制权传递下去,这一设计在机制上强制了绘制顺序的可控性,也为特效之间的叠加提供了明确的语义边界。

当特效链执行完毕后,流程才真正进入渲染后端。无论是 OpenGL 还是其他实现,后端只接收已经完全加工好的渲染数据:几何、纹理、变换矩阵、混合参数。特效系统不直接参与任何 API 级别的绘制调用,这保证了渲染后端的可替换性,同时也限制了特效的能力边界。

综合来看,KWin 的渲染架构体现出几个鲜明特征:

  • 调度与绘制解耦:RenderLoop 控制时序,Compositor 负责流程
  • 阶段明确:每一帧都被拆分为可预测、可插拔的处理阶段
  • 接口驱动:Scene、Effect、Renderer 均以接口定义交互方式
  • 可组合的特效链:通过递归迭代器实现严格顺序的效果叠加
  • 后端无感知特效:渲染后端只关心“画什么”,不关心“为什么这么画”

本文只完成了对整体流程和结构的梳理,并未深入到具体的数据结构和渲染实现细节。但在理解了这条主干之后,再回头分析某一个特效、某一次性能回退,或某个渲染异常,其定位成本都会显著降低。

Reply Favorite View the author
All Replies
BLumia
deepin
2025-12-26 10:25
#1

KWin (怀旧服)是如何渲染的?

Reply View the author
yicold
deepin product team
2025-12-26 11:36
#2

被标题耽搁的帖子,消灭二楼回复。applaud

Reply View the author
HualetWang
deepin
2025-12-26 11:45
#3

不是总分总结构的文章都不是好文章,从你行文的风格上来看应该比较适合搞直播!😏

Reply View the author
justforlxz
deepin
2025-12-26 13:13
#4
HualetWang

不是总分总结构的文章都不是好文章,从你行文的风格上来看应该比较适合搞直播!😏

太长时间没写了,生疏了smirk

Reply View the author
神末shenmo
deepin
Spark-App
Q&A Team
2025-12-26 14:20
#5

这个就叫专业

Reply View the author
justforlxz
deepin
2025-12-26 15:26
#6
神末shenmo

这个就叫专业

ceeb653ely1g2e3wjtga1g209g08zh9y.gif

Reply View the author
MeGusta
deepin
2025-12-26 16:36
#7

先mark再看~

6e364b2e59064bb6ad73a441340563b2.gif

Reply View the author