/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2006 Lubos Lunak SPDX-License-Identifier: GPL-2.0-or-later */ #include "compositor_wayland.h" #include "core/brightnessdevice.h" #include "core/graphicsbufferview.h" #include "core/output.h" #include "core/outputbackend.h" #include "core/renderbackend.h" #include "core/renderlayer.h" #include "cursorsource.h" #include "effect/effecthandler.h" #include "ftrace.h" #include "main.h" #include "opengl/glplatform.h" #include "platformsupport/scenes/opengl/openglbackend.h" #include "platformsupport/scenes/qpainter/qpainterbackend.h" #include "scene/cursordelegate_opengl.h" #include "scene/cursordelegate_qpainter.h" #include "scene/cursorscene.h" #include "scene/itemrenderer_opengl.h" #include "scene/itemrenderer_qpainter.h" #include "scene/workspacescene_opengl.h" #include "scene/workspacescene_qpainter.h" #include "window.h" #include "workspace.h" #if KWIN_BUILD_NOTIFICATIONS #include #endif #include #include namespace KWin { WaylandCompositor *WaylandCompositor::create(QObject *parent) { Q_ASSERT(!s_compositor); auto *compositor = new WaylandCompositor(parent); s_compositor = compositor; return compositor; } WaylandCompositor::WaylandCompositor(QObject *parent) : Compositor(parent) { } WaylandCompositor::~WaylandCompositor() { Q_EMIT aboutToDestroy(); stop(); // this can't be called in the destructor of Compositor } bool WaylandCompositor::attemptOpenGLCompositing() { std::unique_ptr backend = kwinApp()->outputBackend()->createOpenGLBackend(); if (!backend) { return false; } if (!backend->isFailed()) { backend->init(); } if (backend->isFailed()) { return false; } const QByteArray forceEnv = qgetenv("KWIN_COMPOSE"); if (!forceEnv.isEmpty()) { if (qstrcmp(forceEnv, "O2") == 0 || qstrcmp(forceEnv, "O2ES") == 0) { qCDebug(KWIN_CORE) << "OpenGL 2 compositing enforced by environment variable"; } else { // OpenGL 2 disabled by environment variable return false; } } else { if (backend->openglContext()->glPlatform()->recommendedCompositor() < OpenGLCompositing) { qCDebug(KWIN_CORE) << "Driver does not recommend OpenGL compositing"; return false; } } // We only support the OpenGL 2+ shader API, not GL_ARB_shader_objects if (!backend->openglContext()->hasVersion(Version(2, 0))) { qCDebug(KWIN_CORE) << "OpenGL 2.0 is not supported"; return false; } m_backend = std::move(backend); qCDebug(KWIN_CORE) << "OpenGL compositing has been successfully initialized"; return true; } bool WaylandCompositor::attemptQPainterCompositing() { std::unique_ptr backend(kwinApp()->outputBackend()->createQPainterBackend()); if (!backend || backend->isFailed()) { return false; } m_backend = std::move(backend); qCDebug(KWIN_CORE) << "QPainter compositing has been successfully initialized"; return true; } void WaylandCompositor::createRenderer() { // If compositing has been restarted, try to use the last used compositing type. const QList availableCompositors = kwinApp()->outputBackend()->supportedCompositors(); QList candidateCompositors; if (m_selectedCompositor != NoCompositing) { candidateCompositors.append(m_selectedCompositor); } else { candidateCompositors = availableCompositors; const auto userConfigIt = std::find(candidateCompositors.begin(), candidateCompositors.end(), options->compositingMode()); if (userConfigIt != candidateCompositors.end()) { candidateCompositors.erase(userConfigIt); candidateCompositors.prepend(options->compositingMode()); } else { qCWarning(KWIN_CORE) << "Configured compositor not supported by Platform. Falling back to defaults"; } } for (auto type : std::as_const(candidateCompositors)) { bool stop = false; switch (type) { case OpenGLCompositing: qCDebug(KWIN_CORE) << "Attempting to load the OpenGL scene"; stop = attemptOpenGLCompositing(); break; case QPainterCompositing: qCDebug(KWIN_CORE) << "Attempting to load the QPainter scene"; stop = attemptQPainterCompositing(); break; case NoCompositing: qCDebug(KWIN_CORE) << "Starting without compositing..."; stop = true; break; } if (stop) { break; } else if (qEnvironmentVariableIsSet("KWIN_COMPOSE")) { qCCritical(KWIN_CORE) << "Could not fulfill the requested compositing mode in KWIN_COMPOSE:" << type << ". Exiting."; qApp->quit(); } } } void WaylandCompositor::createScene() { if (const auto openglBackend = qobject_cast(m_backend.get())) { m_scene = std::make_unique(openglBackend); m_cursorScene = std::make_unique(std::make_unique(openglBackend->eglDisplayObject())); } else { const auto qpainterBackend = static_cast(m_backend.get()); m_scene = std::make_unique(qpainterBackend); m_cursorScene = std::make_unique(std::make_unique()); } Q_EMIT sceneCreated(); } void WaylandCompositor::start() { if (kwinApp()->isTerminating()) { return; } if (m_state != State::Off) { return; } Q_EMIT aboutToToggleCompositing(); m_state = State::Starting; if (!m_backend) { createRenderer(); } if (!m_backend) { m_state = State::Off; qCCritical(KWIN_CORE) << "The used windowing system requires compositing"; qCCritical(KWIN_CORE) << "We are going to quit KWin now as it is broken"; qApp->quit(); return; } if (m_selectedCompositor == NoCompositing) { m_selectedCompositor = m_backend->compositingType(); switch (m_selectedCompositor) { case NoCompositing: break; case OpenGLCompositing: QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); break; case QPainterCompositing: QQuickWindow::setGraphicsApi(QSGRendererInterface::Software); break; } } createScene(); const QList outputs = workspace()->outputs(); for (Output *output : outputs) { addOutput(output); } connect(workspace(), &Workspace::outputAdded, this, &WaylandCompositor::addOutput); connect(workspace(), &Workspace::outputRemoved, this, &WaylandCompositor::removeOutput); m_state = State::On; const auto windows = workspace()->windows(); for (Window *window : windows) { window->setupCompositing(); } // Sets also the 'effects' pointer. kwinApp()->createEffectsHandler(this, m_scene.get()); Q_EMIT compositingToggled(true); } void WaylandCompositor::stop() { if (m_state == State::Off || m_state == State::Stopping) { return; } m_state = State::Stopping; Q_EMIT aboutToToggleCompositing(); // Some effects might need access to effect windows when they are about to // be destroyed, for example to unreference deleted windows, so we have to // make sure that effect windows outlive effects. delete effects; effects = nullptr; if (Workspace::self()) { const auto windows = workspace()->windows(); for (Window *window : windows) { window->finishCompositing(); } disconnect(workspace(), &Workspace::outputAdded, this, &WaylandCompositor::addOutput); disconnect(workspace(), &Workspace::outputRemoved, this, &WaylandCompositor::removeOutput); } if (m_backend->compositingType() == OpenGLCompositing) { // some layers need a context current for destruction static_cast(m_backend.get())->makeCurrent(); } const auto superlayers = m_superlayers; for (auto it = superlayers.begin(); it != superlayers.end(); ++it) { removeSuperLayer(*it); } m_scene.reset(); m_cursorScene.reset(); m_backend.reset(); m_state = State::Off; Q_EMIT compositingToggled(false); } static QRect centerBuffer(const QSizeF &bufferSize, const QSize &modeSize) { const double widthScale = bufferSize.width() / double(modeSize.width()); const double heightScale = bufferSize.height() / double(modeSize.height()); if (widthScale > heightScale) { const QSize size = (bufferSize / widthScale).toSize(); const uint32_t yOffset = (modeSize.height() - size.height()) / 2; return QRect(QPoint(0, yOffset), size); } else { const QSize size = (bufferSize / heightScale).toSize(); const uint32_t xOffset = (modeSize.width() - size.width()) / 2; return QRect(QPoint(xOffset, 0), size); } } static bool checkForBlackBackground(SurfaceItem *background) { if (!background->pixmap() || !background->pixmap()->buffer() || !background->pixmap()->buffer()->shmAttributes() || background->pixmap()->buffer()->shmAttributes()->size != QSize(1, 1)) { return false; } const GraphicsBufferView view(background->pixmap()->buffer()); if (!view.image()) { return false; } const QRgb rgb = view.image()->pixel(0, 0); const QVector3D encoded(qRed(rgb) / 255.0, qGreen(rgb) / 255.0, qBlue(rgb) / 255.0); const QVector3D nits = background->colorDescription().mapTo(encoded, ColorDescription(NamedColorimetry::BT709, TransferFunction(TransferFunction::linear), 100, 0, std::nullopt, std::nullopt), background->renderingIntent()); // below 0.1 nits, it shouldn't be noticeable that we replace it with black return nits.lengthSquared() <= (0.1 * 0.1); } void WaylandCompositor::composite(RenderLoop *renderLoop) { if (m_backend->checkGraphicsReset()) { qCDebug(KWIN_CORE) << "Graphics reset occurred"; #if KWIN_BUILD_NOTIFICATIONS KNotification::event(QStringLiteral("graphicsreset"), i18n("Desktop effects were restarted due to a graphics reset")); #endif reinitialize(); return; } Output *output = findOutput(renderLoop); OutputLayer *primaryLayer = m_backend->primaryLayer(output); fTraceDuration("Paint (", output->name(), ")"); RenderLayer *superLayer = m_superlayers[renderLoop]; superLayer->setOutputLayer(primaryLayer); renderLoop->prepareNewFrame(); auto frame = std::make_shared(renderLoop, std::chrono::nanoseconds(1'000'000'000'000 / output->refreshRate())); bool directScanout = false; std::optional desiredArtificalHdrHeadroom; // brightness animations should be skipped when // - the output is new, and we didn't have the output configuration applied yet // - there's not enough steps to do a smooth animation // - the brightness device is external, most of them do an animation on their own if (!output->currentBrightness().has_value() || (!output->highDynamicRange() && output->brightnessDevice() && !output->isInternal()) || (!output->highDynamicRange() && output->brightnessDevice() && output->brightnessDevice()->brightnessSteps() < 5)) { frame->setBrightness(output->brightnessSetting()); } else { constexpr double changePerSecond = 3; const double maxChangePerFrame = changePerSecond * 1'000.0 / renderLoop->refreshRate(); // brightness perception is non-linear, gamma 2.2 encoding *roughly* represents that const double current = std::pow(*output->currentBrightness(), 1.0 / 2.2); frame->setBrightness(std::pow(std::clamp(std::pow(output->brightnessSetting(), 1.0 / 2.2), current - maxChangePerFrame, current + maxChangePerFrame), 2.2)); } if (primaryLayer->needsRepaint() || superLayer->needsRepaint()) { auto totalTimeQuery = std::make_unique(); renderLoop->beginPaint(); QRegion surfaceDamage = primaryLayer->repaints(); primaryLayer->resetRepaints(); prePaintPass(superLayer, &surfaceDamage); frame->setDamage(surfaceDamage); // slowly adjust the artificial HDR headroom for the next frame // note that this is only done for internal displays, because external displays usually apply slow animations to brightness changes if (!output->highDynamicRange() && output->brightnessDevice() && output->currentBrightness() && output->artificialHdrHeadroom() && output->isInternal() && output->colorProfileSource() != Output::ColorProfileSource::ICC) { const auto desiredHdrHeadroom = superLayer->delegate()->desiredHdrHeadroom(); // just a rough estimate from the Framework 13 laptop. The less accurate this is, the more the screen will flicker during backlight changes constexpr double relativeLuminanceAtZeroBrightness = 0.04; // the higher this is, the more likely the user is to notice the change in backlight brightness // at the same time, if it's too low, it takes ages until the user sees the HDR effect constexpr double changePerSecond = 0.5; // to restrict HDR videos from using all the battery and burning your eyes // TODO make it a setting, and/or dependent on the power management state? constexpr double maxHdrHeadroom = 3.0; // = the headroom at 100% backlight const double maxPossibleHeadroom = (1 + relativeLuminanceAtZeroBrightness) / (relativeLuminanceAtZeroBrightness + *output->currentBrightness()); desiredArtificalHdrHeadroom = std::clamp(desiredHdrHeadroom, 1.0, std::min(maxPossibleHeadroom, maxHdrHeadroom)); const double changePerFrame = changePerSecond * double(frame->refreshDuration().count()) / 1'000'000'000; const double newHeadroom = std::clamp(*desiredArtificalHdrHeadroom, output->artificialHdrHeadroom() - changePerFrame, output->artificialHdrHeadroom() + changePerFrame); frame->setArtificialHdrHeadroom(newHeadroom); } else { frame->setArtificialHdrHeadroom(1); } Window *const activeWindow = workspace()->activeWindow(); SurfaceItem *const activeFullscreenItem = activeWindow && activeWindow->isFullScreen() && activeWindow->isOnOutput(output) ? activeWindow->surfaceItem() : nullptr; frame->setContentType(activeWindow && activeFullscreenItem ? activeFullscreenItem->contentType() : ContentType::None); const bool wantsAdaptiveSync = activeWindow && activeWindow->isOnOutput(output) && activeWindow->wantsAdaptiveSync(); const bool vrr = (output->capabilities() & Output::Capability::Vrr) && (output->vrrPolicy() == VrrPolicy::Always || (output->vrrPolicy() == VrrPolicy::Automatic && wantsAdaptiveSync)); const bool tearing = (output->capabilities() & Output::Capability::Tearing) && options->allowTearing() && activeFullscreenItem && activeWindow->wantsTearing(activeFullscreenItem->presentationHint() == PresentationModeHint::Async); if (vrr) { frame->setPresentationMode(tearing ? PresentationMode::AdaptiveAsync : PresentationMode::AdaptiveSync); } else { frame->setPresentationMode(tearing ? PresentationMode::Async : PresentationMode::VSync); } const uint32_t planeCount = 1; if (const auto scanoutCandidates = superLayer->delegate()->scanoutCandidates(planeCount + 1); !scanoutCandidates.isEmpty()) { const auto sublayers = superLayer->sublayers(); bool scanoutPossible = std::none_of(sublayers.begin(), sublayers.end(), [](RenderLayer *sublayer) { return sublayer->isVisible(); }); if (scanoutCandidates.size() > planeCount) { scanoutPossible &= checkForBlackBackground(scanoutCandidates.back()); } if (scanoutPossible) { primaryLayer->setTargetRect(centerBuffer(output->transform().map(scanoutCandidates.front()->size()), output->modeSize())); directScanout = primaryLayer->importScanoutBuffer(scanoutCandidates.front(), frame); if (directScanout) { // if present works, we don't want to touch the frame object again afterwards, // so end the time query here instead of later totalTimeQuery->end(); frame->addRenderTimeQuery(std::move(totalTimeQuery)); totalTimeQuery = std::make_unique(); directScanout &= m_backend->present(output, frame); } } } else { primaryLayer->notifyNoScanoutCandidate(); } if (!directScanout) { primaryLayer->setTargetRect(QRect(QPoint(0, 0), output->modeSize())); if (auto beginInfo = primaryLayer->beginFrame()) { auto &[renderTarget, repaint] = beginInfo.value(); const QRegion bufferDamage = surfaceDamage.united(repaint).intersected(superLayer->rect().toAlignedRect()); paintPass(superLayer, renderTarget, bufferDamage); primaryLayer->endFrame(bufferDamage, surfaceDamage, frame.get()); } } postPaintPass(superLayer); if (!directScanout) { totalTimeQuery->end(); frame->addRenderTimeQuery(std::move(totalTimeQuery)); } } if (!directScanout) { if (!m_backend->present(output, frame)) { m_backend->repairPresentation(output); } } framePass(superLayer, frame.get()); if ((frame->brightness() && std::abs(*frame->brightness() - output->brightnessSetting()) > 0.001) || (desiredArtificalHdrHeadroom && frame->artificialHdrHeadroom() && std::abs(*frame->artificialHdrHeadroom() - *desiredArtificalHdrHeadroom) > 0.001)) { // we're currently running an animation to change the brightness renderLoop->scheduleRepaint(); } // TODO: move this into the cursor layer const auto frameTime = std::chrono::duration_cast(output->renderLoop()->lastPresentationTimestamp()); if (!Cursors::self()->isCursorHidden()) { Cursor *cursor = Cursors::self()->currentCursor(); if (cursor->geometry().intersects(output->geometry())) { if (CursorSource *source = cursor->source()) { source->frame(frameTime); } } } } void WaylandCompositor::addOutput(Output *output) { if (output->isPlaceholder()) { return; } auto workspaceLayer = new RenderLayer(output->renderLoop()); workspaceLayer->setDelegate(std::make_unique(m_scene.get(), output)); workspaceLayer->setGeometry(output->rectF()); connect(output, &Output::geometryChanged, workspaceLayer, [output, workspaceLayer]() { workspaceLayer->setGeometry(output->rectF()); }); auto cursorLayer = new RenderLayer(output->renderLoop()); cursorLayer->setVisible(false); if (m_backend->compositingType() == OpenGLCompositing) { cursorLayer->setDelegate(std::make_unique(m_cursorScene.get(), output)); } else { cursorLayer->setDelegate(std::make_unique(m_cursorScene.get(), output)); } cursorLayer->setParent(workspaceLayer); cursorLayer->setSuperlayer(workspaceLayer); static const bool forceSoftwareCursor = qEnvironmentVariableIntValue("KWIN_FORCE_SW_CURSOR") == 1; auto updateCursorLayer = [this, output, cursorLayer]() { const Cursor *cursor = Cursors::self()->currentCursor(); const QRectF outputLocalRect = output->mapFromGlobal(cursor->geometry()); const auto outputLayer = m_backend->cursorLayer(output); if (!cursor->isOnOutput(output)) { if (outputLayer && outputLayer->isEnabled()) { outputLayer->setEnabled(false); output->updateCursorLayer(); } cursorLayer->setVisible(false); return true; } const auto renderHardwareCursor = [&]() { if (!outputLayer || forceSoftwareCursor) { return false; } QRectF nativeCursorRect = output->transform().map(scaledRect(outputLocalRect, output->scale()), output->pixelSize()); QSize bufferSize(std::ceil(nativeCursorRect.width()), std::ceil(nativeCursorRect.height())); const auto recommendedSizes = outputLayer->recommendedSizes(); if (!recommendedSizes.empty()) { auto bigEnough = recommendedSizes | std::views::filter([bufferSize](const auto &size) { return size.width() >= bufferSize.width() && size.height() >= bufferSize.height(); }); const auto it = std::ranges::min_element(bigEnough, [](const auto &left, const auto &right) { return left.width() * left.height() < right.width() * right.height(); }); if (it == bigEnough.end()) { // no size found, this most likely won't work return false; } bufferSize = *it; nativeCursorRect = output->transform().map(QRectF(outputLocalRect.topLeft() * output->scale(), bufferSize), output->pixelSize()); } outputLayer->setHotspot(output->transform().map(cursor->hotspot() * output->scale(), bufferSize)); outputLayer->setTargetRect(QRect(nativeCursorRect.topLeft().toPoint(), bufferSize)); if (auto beginInfo = outputLayer->beginFrame()) { const RenderTarget &renderTarget = beginInfo->renderTarget; RenderLayer renderLayer(output->renderLoop()); renderLayer.setDelegate(std::make_unique(m_cursorScene.get(), output)); renderLayer.setOutputLayer(outputLayer); renderLayer.delegate()->prePaint(); renderLayer.delegate()->paint(renderTarget, infiniteRegion()); renderLayer.delegate()->postPaint(); if (!outputLayer->endFrame(infiniteRegion(), infiniteRegion(), nullptr)) { return false; } } else { return false; } outputLayer->setEnabled(true); return output->updateCursorLayer(); }; if (renderHardwareCursor()) { cursorLayer->setVisible(false); return true; } else { if (outputLayer && outputLayer->isEnabled()) { outputLayer->setEnabled(false); output->updateCursorLayer(); } cursorLayer->setVisible(cursor->isOnOutput(output)); cursorLayer->setGeometry(outputLocalRect); return false; } }; auto moveCursorLayer = [this, output, cursorLayer, updateCursorLayer]() { const Cursor *cursor = Cursors::self()->currentCursor(); const QRectF outputLocalRect = output->mapFromGlobal(cursor->geometry()); const auto outputLayer = m_backend->cursorLayer(output); bool hardwareCursor = false; const bool shouldBeVisible = cursor->isOnOutput(output); if (outputLayer && !forceSoftwareCursor) { if (shouldBeVisible) { const bool enabledBefore = outputLayer->isEnabled(); if (enabledBefore) { // just move it const QRectF nativeCursorRect = output->transform().map(QRectF(outputLocalRect.topLeft() * output->scale(), outputLayer->targetRect().size()), output->pixelSize()); outputLayer->setTargetRect(QRect(nativeCursorRect.topLeft().toPoint(), outputLayer->targetRect().size())); outputLayer->setEnabled(true); hardwareCursor = output->updateCursorLayer(); if (!hardwareCursor) { outputLayer->setEnabled(false); if (enabledBefore) { output->updateCursorLayer(); } } } else { // do the full update hardwareCursor = updateCursorLayer(); } } else if (outputLayer->isEnabled()) { outputLayer->setEnabled(false); output->updateCursorLayer(); } } cursorLayer->setVisible(shouldBeVisible && !hardwareCursor); cursorLayer->setGeometry(outputLocalRect); }; updateCursorLayer(); connect(output, &Output::geometryChanged, cursorLayer, updateCursorLayer); connect(Cursors::self(), &Cursors::currentCursorChanged, cursorLayer, updateCursorLayer); connect(Cursors::self(), &Cursors::hiddenChanged, cursorLayer, updateCursorLayer); connect(Cursors::self(), &Cursors::positionChanged, cursorLayer, moveCursorLayer); addSuperLayer(workspaceLayer); } void WaylandCompositor::removeOutput(Output *output) { if (output->isPlaceholder()) { return; } removeSuperLayer(m_superlayers[output->renderLoop()]); } } // namespace KWin #include "moc_compositor_wayland.cpp"