/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2015 Martin Gräßlin SPDX-License-Identifier: GPL-2.0-or-later */ #include "drm_output.h" #include "drm_backend.h" #include "drm_connector.h" #include "drm_crtc.h" #include "drm_gpu.h" #include "drm_pipeline.h" #include "core/brightnessdevice.h" #include "core/colortransformation.h" #include "core/iccprofile.h" #include "core/outputconfiguration.h" #include "core/renderbackend.h" #include "core/renderloop.h" #include "core/renderloop_p.h" #include "core/session.h" #include "drm_layer.h" #include "drm_logging.h" #include "utils/envvar.h" #include "utils/kernel.h" // Qt #include #include // c++ #include // drm #include #include #include namespace KWin { static const auto s_disableTripleBuffering = environmentVariableBoolValue("KWIN_DRM_DISABLE_TRIPLE_BUFFERING"); DrmOutput::DrmOutput(const std::shared_ptr &conn, DrmPipeline *pipeline) : BackendOutput(pipeline->gpu()->drmDevice()) , m_gpu(conn->gpu()) , m_pipeline(pipeline) , m_connector(conn) { m_pipeline->setOutput(this); if (m_gpu->atomicModeSetting() && !s_disableTripleBuffering.value_or(false)) { m_renderLoop->setMaxPendingFrameCount(2); } const Edid *edid = m_connector->edid(); setInformation(Information{ .name = m_connector->connectorName(), .manufacturer = edid->manufacturerString(), .model = m_connector->modelName(), .serialNumber = edid->serialNumber(), .eisaId = edid->eisaId(), .physicalSize = m_connector->physicalSize(), .edid = *edid, .subPixel = m_connector->subpixel(), .capabilities = computeCapabilities(), .panelOrientation = m_connector->panelOrientation.isValid() ? DrmConnector::toKWinTransform(m_connector->panelOrientation.enumValue()) : OutputTransform::Normal, .internal = m_connector->isInternal(), .nonDesktop = m_connector->isNonDesktop(), .mstPath = m_connector->mstPath(), .maxPeakBrightness = edid->desiredMaxLuminance(), .maxAverageBrightness = edid->desiredMaxFrameAverageLuminance(), .minBrightness = edid->desiredMinLuminance(), .bitsPerColorRange = BpcRange{ .min = m_gpu->atomicModeSetting() ? uint32_t(m_connector->maxBpc.minValue()) : 8, .max = m_gpu->atomicModeSetting() ? uint32_t(m_connector->maxBpc.maxValue()) : 8, }, .minVrrRefreshRateHz = edid->minVrrRefreshRateHz(), }); updateConnectorProperties(); } bool DrmOutput::addLeaseObjects(QList &objectList) { if (!m_pipeline->crtc()) { qCWarning(KWIN_DRM) << "Can't lease connector: No suitable crtc available"; return false; } qCDebug(KWIN_DRM) << "adding connector" << m_pipeline->connector()->id() << "to lease"; objectList << m_pipeline->connector()->id(); objectList << m_pipeline->crtc()->id(); if (m_pipeline->crtc()->primaryPlane()) { objectList << m_pipeline->crtc()->primaryPlane()->id(); } return true; } void DrmOutput::leased(DrmLease *lease) { m_lease = lease; } void DrmOutput::leaseEnded() { qCDebug(KWIN_DRM) << "ended lease for connector" << m_pipeline->connector()->id(); m_lease = nullptr; } DrmLease *DrmOutput::lease() const { return m_lease; } bool DrmOutput::shouldDisableNonPrimaryPlanes() const { // The kernel rejects async commits that change anything but the primary plane FB_ID // This disables the hardware cursor, so it doesn't interfere with that return m_desiredPresentationMode == PresentationMode::Async || m_desiredPresentationMode == PresentationMode::AdaptiveAsync; } std::expected DrmOutput::presentAsync(OutputLayer *layer, std::optional allowedVrrDelay) { if (!m_pipeline) { // this can happen when the output gets hot-unplugged // FIXME fix output lifetimes so that this doesn't happen anymore... return std::unexpected(OutputError{ .code = OutputErrorCode::InvalidApiUsage, .message = QStringLiteral("FIXME"), }); } if (m_pipeline->gpu()->atomicModeSetting() && shouldDisableNonPrimaryPlanes() && layer->isEnabled()) { return std::unexpected(OutputError{ .code = OutputErrorCode::AsyncFlipFailure, .message = QStringLiteral("Can't use non-primary planes with tearing"), }); } return m_pipeline->presentAsync(layer, allowedVrrDelay); } DrmPlane::Transformations outputToPlaneTransform(OutputTransform transform) { using PlaneTrans = DrmPlane::Transformation; switch (transform.kind()) { case OutputTransform::Normal: return PlaneTrans::Rotate0; case OutputTransform::FlipX: return PlaneTrans::ReflectX | PlaneTrans::Rotate0; case OutputTransform::Rotate90: return PlaneTrans::Rotate90; case OutputTransform::FlipX90: return PlaneTrans::ReflectX | PlaneTrans::Rotate90; case OutputTransform::Rotate180: return PlaneTrans::Rotate180; case OutputTransform::FlipX180: return PlaneTrans::ReflectX | PlaneTrans::Rotate180; case OutputTransform::Rotate270: return PlaneTrans::Rotate270; case OutputTransform::FlipX270: return PlaneTrans::ReflectX | PlaneTrans::Rotate270; default: Q_UNREACHABLE(); } } void DrmOutput::updateConnectorProperties() { updateInformation(); State next = m_state; refreshModes(&next); maybeFixCurrentMode(&next); setState(next); } void DrmOutput::refreshModes(State *nextState) const { const auto previousModes = nextState->modes; nextState->modes.clear(); const auto drmModes = m_pipeline->connector()->modes(); for (const auto &drmMode : drmModes) { nextState->modes.append(drmMode); } for (const auto &modeline : nextState->customModes) { if (auto mode = modeline.match(previousModes)) { nextState->modes.append(mode); } else { nextState->modes.append(m_pipeline->connector()->generateMode(modeline)); } } } void DrmOutput::maybeFixCurrentMode(State *next) const { if (!next->currentMode) { next->currentMode = next->modes.constFirst(); } else if (!next->modes.contains(next->currentMode)) { next->currentMode->setRemoved(); next->modes.push_front(next->currentMode); } } static const bool s_allowColorspaceIntel = qEnvironmentVariableIntValue("KWIN_DRM_ALLOW_INTEL_COLORSPACE") == 1; static const bool s_allowColorspaceNVidia = qEnvironmentVariableIntValue("KWIN_DRM_ALLOW_NVIDIA_COLORSPACE") == 1; BackendOutput::Capabilities DrmOutput::computeCapabilities() const { Capabilities capabilities = Capability::Dpms | Capability::IccProfile | Capability::CustomModes | Capability::HdrIccProfile | Capability::Disable; if (m_connector->overscan.isValid() || m_connector->underscan.isValid()) { capabilities |= Capability::Overscan; } if (m_connector->vrrCapable.isValid() && m_connector->vrrCapable.value()) { capabilities |= Capability::Vrr; } if (m_gpu->asyncPageflipSupported()) { capabilities |= Capability::Tearing; } if (m_connector->broadcastRGB.isValid()) { capabilities |= Capability::RgbRange; } if (m_connector->colorspace.isValid() && (m_connector->colorspace.hasEnum(DrmConnector::Colorspace::BT2020_RGB) || m_connector->colorspace.hasEnum(DrmConnector::Colorspace::BT2020_YCC)) && m_connector->edid()->supportsBT2020()) { bool allowColorspace = true; if (m_gpu->drmDevice()->isI915()) { allowColorspace &= s_allowColorspaceIntel || linuxKernelVersion() >= Version(6, 11); } else if (m_gpu->drmDevice()->isNvidia()) { allowColorspace &= s_allowColorspaceNVidia || m_gpu->nvidiaDriverVersion() >= Version(565, 57, 1); } if (allowColorspace) { capabilities |= Capability::WideColorGamut; } } if (m_connector->hdrMetadata.isValid() && m_connector->edid()->supportsPQ() && (capabilities & Capability::WideColorGamut)) { capabilities |= Capability::HighDynamicRange; } if (m_connector->isInternal() && m_autoRotateAvailable) { capabilities |= Capability::AutoRotation; } if (m_state.highDynamicRange || m_state.brightnessDevice || m_state.allowSdrSoftwareBrightness) { capabilities |= Capability::BrightnessControl; // changing brightness too often with DDC/CI can cause problems on // some displays, so automatic brightness is blocked for them const bool ddcci = !m_state.highDynamicRange && m_state.brightnessDevice && m_state.brightnessDevice->usesDdcCi(); if (m_autoBrightnessAvailable && !ddcci) { capabilities |= Capability::AutomaticBrightness; } } if (m_connector->edid()->isValid() && m_connector->edid()->nativeColorimetry().has_value()) { capabilities |= Capability::BuiltInColorProfile; } if (m_state.detectedDdcCi) { capabilities |= Capability::DdcCi; } if (m_connector->maxBpc.isValid()) { capabilities |= Capability::MaxBitsPerColor; } if (m_state.brightnessDevice && isInternal() && !(capabilities & Capability::HighDynamicRange)) { capabilities |= Capability::Edr; } if (m_gpu->sharpnessSupported()) { capabilities |= Capability::SharpnessControl; } if (m_connector->abmLevel.isValid()) { capabilities |= Capability::AbmLevel; } return capabilities; } void DrmOutput::updateInformation() { // not all changes are currently handled by the rest of KWin // so limit the changes to what's verified to work const Edid *edid = m_connector->edid(); Information nextInfo = m_information; nextInfo.capabilities = computeCapabilities(); nextInfo.maxPeakBrightness = edid->desiredMaxLuminance(); nextInfo.maxAverageBrightness = edid->desiredMaxFrameAverageLuminance(); nextInfo.minBrightness = edid->desiredMinLuminance(); // TODO narrow that down by parsing the EDID and checking what the display supports nextInfo.bitsPerColorRange = BpcRange{ .min = m_gpu->atomicModeSetting() ? uint32_t(m_connector->maxBpc.minValue()) : 8, .max = m_gpu->atomicModeSetting() ? uint32_t(m_connector->maxBpc.maxValue()) : 8, }; setInformation(nextInfo); } std::expected DrmOutput::testPresentation(const std::shared_ptr &frame) { m_desiredPresentationMode = frame->presentationMode(); const auto layers = m_pipeline->layers(); const bool nonPrimaryEnabled = std::ranges::any_of(layers, [](OutputLayer *layer) { return layer->isEnabled() && layer->type() != OutputLayerType::Primary; }); if (m_gpu->needsModeset()) { // modesets should be done with only the primary plane // as additional planes may mean we can't power all outputs if (nonPrimaryEnabled) { return std::unexpected(OutputError{ .code = OutputErrorCode::NeedsModeset, .message = QStringLiteral("Modesets should only be done with the primary plane"), }); } // the atomic test for the modeset has already been done before // so testing again isn't super useful return {}; } m_pipeline->setPresentationMode(frame->presentationMode()); if (nonPrimaryEnabled) { // the cursor plane needs to be disabled before we enable tearing; see DrmOutput::presentAsync if (frame->presentationMode() == PresentationMode::AdaptiveAsync) { m_pipeline->setPresentationMode(PresentationMode::AdaptiveSync); } else if (frame->presentationMode() == PresentationMode::Async) { m_pipeline->setPresentationMode(PresentationMode::VSync); } } auto ret = m_pipeline->testPresent(frame); if (!ret && frame->presentationMode() == PresentationMode::AdaptiveAsync) { // tearing can fail in various circumstances, but vrr shouldn't m_pipeline->setPresentationMode(PresentationMode::AdaptiveSync); ret = m_pipeline->testPresent(frame); } if (!ret && frame->presentationMode() != PresentationMode::VSync) { // retry with the most basic presentation mode m_pipeline->setPresentationMode(PresentationMode::VSync); ret = m_pipeline->testPresent(frame); } return ret; } std::expected DrmOutput::present(const QList &layersToUpdate, const std::shared_ptr &frame) { m_desiredPresentationMode = frame->presentationMode(); const bool needsModeset = m_gpu->needsModeset(); std::expected ret; if (needsModeset) { m_pipeline->setPresentationMode(PresentationMode::VSync); m_pipeline->setContentType(DrmConnector::DrmContentType::Graphics); m_pipeline->maybeModeset(frame); } else { // the presentation mode of the pipeline is already set in testPresentation ret = m_pipeline->present(layersToUpdate, frame); } m_renderLoop->setPresentationMode(m_pipeline->presentationMode()); if (!ret) { return ret; } updateBrightness(frame->brightness().value_or(m_state.currentBrightness.value_or(m_state.brightnessSetting)), frame->artificialHdrHeadroom().value_or(m_state.artificialHdrHeadroom), frame->dimmingFactor().value_or(m_state.currentDimming)); return ret; } void DrmOutput::repairPresentation() { // read back drm properties, most likely our info is out of date somehow // or we need a modeset QTimer::singleShot(0, m_gpu->platform(), [backend = m_gpu->platform()]() { backend->updateOutputs(); }); } bool DrmOutput::recommendsOverlayUse() const { // Two drivers are known to have issues with overlays: // - amdgpu on some hardware has really slow atomic tests when overlays are involved // - Nvidia has more severe issues if (PROJECT_VERSION_PATCH >= 80) { // we can take some more risks in development versions. return !m_gpu->drmDevice()->isNvidia(); } else { // release version: play it really safe for now, only enable on select drivers return m_gpu->drmDevice()->isI915() || m_gpu->drmDevice()->isIntelXE(); } } DrmConnector *DrmOutput::connector() const { return m_connector.get(); } DrmPipeline *DrmOutput::pipeline() const { return m_pipeline; } std::optional DrmOutput::decideAutomaticBpcLimit() const { static bool preferreedColorDepthIsSet = false; static const int preferred = qEnvironmentVariableIntValue("KWIN_DRM_PREFER_COLOR_DEPTH", &preferreedColorDepthIsSet); if (preferreedColorDepthIsSet) { return preferred / 3; } if (!m_connector->mstPath().isEmpty()) { // >8bpc is often broken with docks return 8; } return std::nullopt; } static QVector3D adaptChannelFactors(const std::shared_ptr &originalColor, const QVector3D &sRGBchannelFactors) { QVector3D adaptedChannelFactors = ColorDescription::sRGB->containerColorimetry().relativeColorimetricTo(originalColor->containerColorimetry()) * sRGBchannelFactors; // ensure none of the values reach zero, otherwise the white point might end up on or outside // the edges of the gamut, which leads to terrible glitches adaptedChannelFactors.setX(std::max(adaptedChannelFactors.x(), 0.0001f)); adaptedChannelFactors.setY(std::max(adaptedChannelFactors.y(), 0.0001f)); adaptedChannelFactors.setZ(std::max(adaptedChannelFactors.z(), 0.0001f)); return adaptedChannelFactors; } static std::shared_ptr applyNightLight(const std::shared_ptr &originalColor, const QVector3D &sRGBchannelFactors) { const QVector3D adapted = adaptChannelFactors(originalColor, sRGBchannelFactors); // calculate the white point // this includes the maximum brightness we can do without clipping any color channel as well const xyY newWhite = XYZ::fromVector(originalColor->containerColorimetry().toXYZ() * adapted).toxyY(); return originalColor->withWhitepoint(newWhite)->dimmed(newWhite.Y); } double DrmOutput::calculateMaxArtificialHdrHeadroom(const State &next) const { if (!next.brightnessDevice || !isInternal() || next.edrPolicy == EdrPolicy::Never || !(capabilities() & Capability::Edr)) { return 1.0; } const bool effectiveHdr = next.highDynamicRange && (capabilities() & Capability::HighDynamicRange); if (effectiveHdr) { return 1.0; } // 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; // 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; const double maxPossibleHeadroom = (1 + relativeLuminanceAtZeroBrightness) / (relativeLuminanceAtZeroBrightness + next.currentBrightness.value_or(next.brightnessSetting)); return std::min(maxPossibleHeadroom, maxHdrHeadroom); } std::shared_ptr DrmOutput::createColorDescription(const State &next) const { const bool effectiveHdr = next.highDynamicRange && (capabilities() & Capability::HighDynamicRange); const bool effectiveWcg = next.wideColorGamut && (capabilities() & Capability::WideColorGamut); double brightnessFactor = 1.0; if ((!next.brightnessDevice && next.allowSdrSoftwareBrightness) || effectiveHdr) { brightnessFactor = next.currentBrightness.value_or(next.brightnessSetting); } if (!next.brightnessDevice || next.brightnessDevice->usesDdcCi() || effectiveHdr) { brightnessFactor *= next.currentDimming; } double minLuminance = 0.01; double maxAverageLuminance = 200; double maxLuminance = 200; double maxReferenceLuminance = maxLuminance; TransferFunction transferFunction{TransferFunction::Type::gamma22, minLuminance, maxLuminance}; Colorimetry containerColorimetry = Colorimetry::BT709; Colorimetry nativeColorimetry = Colorimetry::BT709; auto profileSource = effectiveHdr ? next.hdrColorProfileSource : next.colorProfileSource; const auto &iccProfile = effectiveHdr ? next.hdrIccProfile : next.iccProfile; if (!iccProfile && profileSource == ColorProfileSource::ICC) { // can't do anything profileSource = ColorProfileSource::sRGB; } if (effectiveHdr && profileSource == ColorProfileSource::sRGB) { // we don't support HDR without a color profile profileSource = ColorProfileSource::EDID; } if (effectiveWcg && !effectiveHdr && profileSource == ColorProfileSource::ICC) { // there's no dedicated ICC profile for this case profileSource = ColorProfileSource::EDID; } switch (profileSource) { case ColorProfileSource::sRGB: // all the default values are for this case transferFunction = TransferFunction(TransferFunction::Type::gamma22, minLuminance * next.artificialHdrHeadroom, maxLuminance * next.artificialHdrHeadroom); break; case ColorProfileSource::ICC: nativeColorimetry = iccProfile->colorimetry(); if (effectiveHdr) { if (effectiveWcg) { containerColorimetry = Colorimetry::BT2020; } maxAverageLuminance = iccProfile->maxFALL().value_or(maxAverageLuminance); maxLuminance = iccProfile->maxCLL().value_or(maxAverageLuminance); minLuminance = iccProfile->relativeBlackPoint().value_or(0) * maxAverageLuminance; maxReferenceLuminance = maxAverageLuminance; transferFunction = TransferFunction(TransferFunction::gamma22, minLuminance, maxLuminance); } else { containerColorimetry = nativeColorimetry; maxAverageLuminance = iccProfile->maxFALL().value_or(maxAverageLuminance); maxLuminance = maxAverageLuminance; maxReferenceLuminance = maxAverageLuminance; minLuminance = iccProfile->relativeBlackPoint().value_or(0) * maxAverageLuminance; transferFunction = TransferFunction(TransferFunction::gamma22, minLuminance * next.artificialHdrHeadroom, maxLuminance * next.artificialHdrHeadroom); } break; case ColorProfileSource::EDID: if (m_information.edid.isValid() && m_information.edid.nativeColorimetry()) { nativeColorimetry = *m_information.edid.nativeColorimetry(); } if (effectiveWcg) { containerColorimetry = Colorimetry::BT2020; } else { containerColorimetry = nativeColorimetry; } if (effectiveHdr) { transferFunction = TransferFunction(TransferFunction::PerceptualQuantizer); // HDR screens are weird, sending them the min. luminance from the EDID does // *not* make all of them present the darkest luminance the display can show. // To work around that, (unless overridden by the user), assume the min. luminance of the transfer function instead minLuminance = next.minBrightnessOverride.value_or(transferFunction.minLuminance); maxAverageLuminance = next.maxAverageBrightnessOverride.value_or(m_connector->edid()->desiredMaxFrameAverageLuminance().value_or(next.referenceLuminance)); maxLuminance = next.maxPeakBrightnessOverride.value_or(m_connector->edid()->desiredMaxLuminance().value_or(800)); maxReferenceLuminance = next.referenceLuminance; } else { // TODO get luminance values from EDID? transferFunction = TransferFunction(TransferFunction::gamma22, minLuminance * next.artificialHdrHeadroom, maxLuminance * next.artificialHdrHeadroom); } break; } Colorimetry sdrColorimetry = Colorimetry::BT709.interpolateGamutTo(nativeColorimetry, next.sdrGamutWideness); const double effectiveReferenceLuminance = 5 + (maxReferenceLuminance - 5) * brightnessFactor; return std::make_shared(ColorDescription{ containerColorimetry, transferFunction, effectiveReferenceLuminance, minLuminance, maxAverageLuminance * next.maxPossibleArtificialHdrHeadroom, maxLuminance * next.maxPossibleArtificialHdrHeadroom, nativeColorimetry, sdrColorimetry, }); } bool DrmOutput::queueChanges(const std::shared_ptr &props) { m_nextState = m_state; m_nextState->enabled = props->enabled.value_or(m_state.enabled); m_nextState->position = props->pos.value_or(m_state.position); m_nextState->scale = props->scale.value_or(m_state.scale); m_nextState->scaleSetting = props->scaleSetting.value_or(m_state.scaleSetting); m_nextState->transform = props->transform.value_or(m_state.transform); m_nextState->manualTransform = props->manualTransform.value_or(m_state.manualTransform); m_nextState->overscan = props->overscan.value_or(m_state.overscan); m_nextState->rgbRange = props->rgbRange.value_or(m_state.rgbRange); m_nextState->highDynamicRange = props->highDynamicRange.value_or(m_state.highDynamicRange); m_nextState->referenceLuminance = props->referenceLuminance.value_or(m_state.referenceLuminance); m_nextState->wideColorGamut = props->wideColorGamut.value_or(m_state.wideColorGamut); m_nextState->autoRotatePolicy = props->autoRotationPolicy.value_or(m_state.autoRotatePolicy); m_nextState->maxPeakBrightnessOverride = props->maxPeakBrightnessOverride.value_or(m_state.maxPeakBrightnessOverride); m_nextState->maxAverageBrightnessOverride = props->maxAverageBrightnessOverride.value_or(m_state.maxAverageBrightnessOverride); m_nextState->minBrightnessOverride = props->minBrightnessOverride.value_or(m_state.minBrightnessOverride); m_nextState->sdrGamutWideness = props->sdrGamutWideness.value_or(m_state.sdrGamutWideness); m_nextState->iccProfilePath = props->iccProfilePath.value_or(m_state.iccProfilePath); m_nextState->iccProfile = props->iccProfile.value_or(m_state.iccProfile); m_nextState->hdrIccProfilePath = props->hdrIccProfilePath.value_or(m_state.hdrIccProfilePath); m_nextState->hdrIccProfile = props->hdrIccProfile.value_or(m_state.hdrIccProfile); m_nextState->vrrPolicy = props->vrrPolicy.value_or(m_state.vrrPolicy); m_nextState->colorProfileSource = props->colorProfileSource.value_or(m_state.colorProfileSource); m_nextState->hdrColorProfileSource = props->hdrColorProfileSource.value_or(m_state.hdrColorProfileSource); m_nextState->brightnessSetting = props->brightness.value_or(m_state.brightnessSetting); m_nextState->desiredMode = props->desiredMode.value_or(m_state.desiredMode); m_nextState->allowSdrSoftwareBrightness = props->allowSdrSoftwareBrightness.value_or(m_state.allowSdrSoftwareBrightness); m_nextState->colorPowerTradeoff = props->colorPowerTradeoff.value_or(m_state.colorPowerTradeoff); m_nextState->dimming = props->dimming.value_or(m_state.dimming); m_nextState->brightnessDevice = props->brightnessDevice.value_or(m_state.brightnessDevice); if (!m_nextState->highDynamicRange && m_nextState->brightnessDevice) { m_nextState->currentBrightness = props->currentHardwareBrightness.has_value() ? props->currentHardwareBrightness : m_state.currentBrightness; } m_nextState->uuid = props->uuid.value_or(m_state.uuid); m_nextState->replicationSource = props->replicationSource.value_or(m_state.replicationSource); m_nextState->detectedDdcCi = props->detectedDdcCi.value_or(m_state.detectedDdcCi); m_nextState->allowDdcCi = props->allowDdcCi.value_or(m_state.allowDdcCi); if (m_nextState->allowSdrSoftwareBrightness != m_state.allowSdrSoftwareBrightness) { // make sure that we set the brightness again next frame m_nextState->currentBrightness.reset(); } m_nextState->maxBitsPerColor = props->maxBitsPerColor.value_or(m_state.maxBitsPerColor); m_nextState->automaticMaxBitsPerColorLimit = decideAutomaticBpcLimit(); m_nextState->edrPolicy = props->edrPolicy.value_or(m_state.edrPolicy); m_nextState->dpmsMode = props->dpmsMode.value_or(m_state.dpmsMode); if (props->customModes) { m_nextState->customModes = *props->customModes | std::views::transform([](const OutputModeline &modeline) { return modeline.withFlags(OutputModeline::Flag::Generated | OutputModeline::Flag::Custom); }) | std::ranges::to(); refreshModes(&*m_nextState); } if (props->currentMode) { m_nextState->currentMode = props->currentMode->match(m_nextState->modes); } if (props->customModes || props->currentMode) { maybeFixCurrentMode(&*m_nextState); } m_nextState->maxPossibleArtificialHdrHeadroom = calculateMaxArtificialHdrHeadroom(*m_nextState); m_nextState->originalColorDescription = createColorDescription(*m_nextState); m_nextState->colorDescription = applyNightLight(m_nextState->originalColorDescription, m_sRgbChannelFactors); m_nextState->sharpnessSetting = props->sharpness.value_or(m_state.sharpnessSetting); m_nextState->priority = props->priority.value_or(m_state.priority); m_nextState->deviceOffset = props->deviceOffset.value_or(m_state.deviceOffset); m_nextState->automaticBrightness = props->automaticBrightness.value_or(m_state.automaticBrightness); m_nextState->lastBrightnessAdjustmentReason = props->brightnessReason.value_or(m_state.lastBrightnessAdjustmentReason); m_nextState->autoBrightnessCurve = props->autoBrightnessCurve.value_or(m_state.autoBrightnessCurve); m_nextState->abmLevel = props->abmLevel.value_or(m_state.abmLevel); const bool bt2020 = m_nextState->wideColorGamut && (capabilities() & Capability::WideColorGamut); const bool hdr = m_nextState->highDynamicRange && (capabilities() & Capability::HighDynamicRange); m_pipeline->setMode(std::static_pointer_cast(m_nextState->currentMode)); m_pipeline->setOverscan(m_nextState->overscan); m_pipeline->setRgbRange(m_nextState->rgbRange); m_pipeline->setEnable(m_nextState->enabled); m_pipeline->setActive(m_nextState->enabled && m_nextState->dpmsMode == DpmsMode::On); m_pipeline->setHighDynamicRange(hdr); m_pipeline->setWideColorGamut(bt2020); if (uint32_t bpcSetting = props->maxBitsPerColor.value_or(maxBitsPerColor())) { m_pipeline->setMaxBpc(bpcSetting); } else { const auto tradeoff = props->colorPowerTradeoff.value_or(m_state.colorPowerTradeoff); m_pipeline->setMaxBpc(decideAutomaticBpcLimit().value_or(tradeoff == ColorPowerTradeoff::PreferAccuracy ? 16 : 10)); } if (hdr && m_nextState->hdrColorProfileSource == ColorProfileSource::ICC) { m_pipeline->setIccProfile(m_nextState->hdrIccProfile); } else if (!hdr && !bt2020 && m_nextState->colorProfileSource == ColorProfileSource::ICC) { m_pipeline->setIccProfile(m_nextState->iccProfile); } else { m_pipeline->setIccProfile(nullptr); } // remove the color pipeline for the atomic test // otherwise it could potentially fail if (m_gpu->atomicModeSetting()) { m_pipeline->setCrtcColorPipeline(ColorPipeline{}); } return true; } void DrmOutput::applyQueuedChanges(const std::shared_ptr &props) { Q_EMIT aboutToChange(props.get()); m_pipeline->applyPendingChanges(); tryKmsColorOffloading(*m_nextState); maybeScheduleRepaints(*m_nextState); const bool nextOff = m_nextState->dpmsMode != DpmsMode::On; const bool currentOff = m_state.dpmsMode != DpmsMode::On; if (nextOff != currentOff) { if (m_nextState->dpmsMode == DpmsMode::On) { m_renderLoop->uninhibit(); } else { // NOTE that legacy modesetting applies dpms in the "test" before this // method gets called, so we have to special case legacy vs. atomic here if (m_state.dpmsMode == DpmsMode::On && m_gpu->atomicModeSetting()) { m_nextState->dpmsMode = DpmsMode::TurningOff; } m_renderLoop->inhibit(); } } setState(*m_nextState); m_nextState.reset(); // allowSdrSoftwareBrightness, the brightness device or detectedDdcCi might change our capabilities Information newInfo = m_information; newInfo.capabilities = computeCapabilities(); setInformation(newInfo); if (!m_pipeline->activePending() && m_gpu->needsModeset()) { // If the output is active, state changes end up being committed when presenting. // However, if it's off, that needs to be done explicitly m_gpu->maybeModeset(nullptr, nullptr); } m_renderLoop->setRefreshRate(refreshRate()); if (m_state.brightnessDevice && m_state.highDynamicRange && isInternal()) { // This is usually not necessary with external monitors, as they default to 100% in HDR mode on their own, // and is known to even cause problems with some buggy ones. // This is however needed for laptop displays to have the desired luminance levels m_state.brightnessDevice->setBrightness(1.0); } Q_EMIT changed(); } void DrmOutput::unsetBrightnessDevice() { State next = m_state; next.brightnessDevice = nullptr; setState(next); updateInformation(); } void DrmOutput::updateBrightness(double newBrightness, double newArtificialHdrHeadroom, double newDimming) { if (m_state.currentBrightness == newBrightness && m_state.artificialHdrHeadroom == newArtificialHdrHeadroom && m_state.currentDimming == newDimming) { return; } if (m_state.brightnessDevice && !m_state.highDynamicRange) { double brightnessFactor = newBrightness; // With DDC/CI, dimming should be applied in software only, // to avoid unnecessary EEPROM writes on the display side if (!m_state.brightnessDevice->usesDdcCi()) { brightnessFactor *= newDimming; } constexpr double minLuminance = 0.04; const double effectiveBrightness = (minLuminance + brightnessFactor) * newArtificialHdrHeadroom - minLuminance; m_state.brightnessDevice->setBrightness(effectiveBrightness); } State next = m_state; next.currentBrightness = newBrightness; next.artificialHdrHeadroom = newArtificialHdrHeadroom; next.currentDimming = newDimming; next.maxPossibleArtificialHdrHeadroom = calculateMaxArtificialHdrHeadroom(next); next.originalColorDescription = createColorDescription(next); next.colorDescription = applyNightLight(next.originalColorDescription, m_sRgbChannelFactors); tryKmsColorOffloading(next); setState(next); } void DrmOutput::revertQueuedChanges() { m_nextState.reset(); m_pipeline->revertPendingChanges(); } void DrmOutput::setChannelFactors(const QVector3D &rgb) { if (rgb == m_sRgbChannelFactors) { return; } m_sRgbChannelFactors = rgb; State next = m_state; next.colorDescription = applyNightLight(next.originalColorDescription, m_sRgbChannelFactors); tryKmsColorOffloading(next); setState(next); } void DrmOutput::tryKmsColorOffloading(State &next) { if (!m_pipeline->activePending() || m_pipeline->layers().empty() || m_lease) { return; } const auto repaints = qScopeGuard([this, &next]() { maybeScheduleRepaints(next); // the base was just recomputed, so any offload is gone; the compositor requests it // again every frame in Compositor::setupLayers m_baseCrtcColorPipeline = m_pipeline->crtcColorPipeline(); m_baseLayerBlendingColor = next.layerBlendingColor; m_appliedPostBlendPipeline = ColorPipeline{}; m_appliedLayerBlendingColor = nullptr; }); constexpr TransferFunction::Type blendingSpace = TransferFunction::gamma22; const double maxLuminance = next.colorDescription->maxHdrLuminance().value_or(next.colorDescription->referenceLuminance()); if (next.colorDescription->transferFunction().type == blendingSpace) { next.blendingColor = next.colorDescription; } else { next.blendingColor = next.colorDescription->withTransferFunction(TransferFunction(blendingSpace, 0, maxLuminance)); } // we can't use the original color description without modifications // as that would un-do any brightness adjustments we did for night light // note that we also can't use ColorDescription::dimmed, as we must avoid clipping to this luminance! const auto encoding = next.originalColorDescription->withReference(next.colorDescription->referenceLuminance()); // absolute colorimetric to preserve the whitepoint adjustments made during compositing ColorPipeline colorPipeline = ColorPipeline::create(next.blendingColor, encoding, RenderingIntent::AbsoluteColorimetricNoAdaptation, ColorPipeline::InputType::FixedPoint); const bool hdr = next.highDynamicRange && (capabilities() & Capability::HighDynamicRange); const auto &iccProfile = hdr ? next.hdrIccProfile : next.iccProfile; const ColorProfileSource profileSource = hdr ? next.hdrColorProfileSource : next.colorProfileSource; const bool usesICC = profileSource == ColorProfileSource::ICC && iccProfile; if (next.colorPowerTradeoff == ColorPowerTradeoff::PreferAccuracy) { next.layerBlendingColor = encoding; m_pipeline->setCrtcColorPipeline(ColorPipeline{}); m_pipeline->applyPendingChanges(); m_needsShadowBuffer = usesICC || next.colorDescription->transferFunction().type != blendingSpace || !colorPipeline.isIdentity(); return; } if (usesICC) { colorPipeline.addTransferFunction(encoding->transferFunction(), ColorspaceType::LinearRGB); if (iccProfile->hasMHC2Tag()) { const auto calibration = wireColor(next).fromXYZ() * iccProfile->mhc2Matrix() * encoding->containerColorimetry().toXYZ(); colorPipeline.addMatrix(calibration, colorPipeline.currentOutputRange(), ColorspaceType::LinearRGB); if (auto wireTF = wireTransfer(next); wireTF == TransferFunction::PerceptualQuantizer) { colorPipeline.addInverseTransferFunction(TransferFunction(wireTF, 0, 10'000), ColorspaceType::NonLinearRGB); } else { colorPipeline.addMultiplier(1.0 / encoding->transferFunction().maxLuminance); colorPipeline.addInverseTransferFunction(TransferFunction(TransferFunction::gamma22, 0, 1.0), ColorspaceType::NonLinearRGB); } } else { colorPipeline.addMultiplier(1.0 / encoding->transferFunction().maxLuminance); colorPipeline.add1DLUT(iccProfile->inverseTransferFunction(), ColorspaceType::NonLinearRGB); } if (iccProfile->vcgt()) { colorPipeline.add1DLUT(iccProfile->vcgt(), ColorspaceType::NonLinearRGB); } } m_pipeline->setCrtcColorPipeline(colorPipeline); if (DrmPipeline::commitPipelines({m_pipeline}, m_gpu, DrmPipeline::CommitMode::Test)) { m_pipeline->applyPendingChanges(); next.layerBlendingColor = next.blendingColor; m_needsShadowBuffer = false; return; } if (next.colorDescription->transferFunction().type == blendingSpace && !usesICC) { // Allow falling back to applying night light in non-linear space. // This isn't technically correct, but the difference is quite small and not worth // losing a lot of performance and battery life over ColorPipeline simplerPipeline(ValueRange{0, 1}, ColorspaceType::NonLinearRGB); simplerPipeline.addMatrix(next.blendingColor->toOther(*encoding, RenderingIntent::AbsoluteColorimetricNoAdaptation), colorPipeline.currentOutputRange(), ColorspaceType::NonLinearRGB); m_pipeline->setCrtcColorPipeline(simplerPipeline); if (DrmPipeline::commitPipelines({m_pipeline}, m_gpu, DrmPipeline::CommitMode::Test)) { m_pipeline->applyPendingChanges(); next.layerBlendingColor = next.blendingColor; m_needsShadowBuffer = false; return; } } // fall back to using a shadow buffer for doing blending in gamma 2.2 and/or night light m_pipeline->setCrtcColorPipeline(ColorPipeline{}); m_pipeline->applyPendingChanges(); next.layerBlendingColor = encoding; m_needsShadowBuffer = usesICC || next.colorDescription->transferFunction().type != blendingSpace || !colorPipeline.isIdentity(); } bool DrmOutput::setPostBlendPipeline(const ColorPipeline &pipeline, const std::shared_ptr &newLayerBlendColor) { // like in tryKmsColorOffloading, and additionally only when we're doing color management // on the CRTC, instead of in the shadow buffer if (!m_pipeline->activePending() || !m_pipeline->gpu()->atomicModeSetting() || !m_gpu->colorPipelineSupported() || m_pipeline->layers().empty() || m_lease || m_needsShadowBuffer) { return false; } // steady state: the same offload is already applied, don't re-test the commit every frame if (pipeline == m_appliedPostBlendPipeline && newLayerBlendColor == m_appliedLayerBlendingColor) { return true; } // the compositor's pipeline goes newLayerBlendColor -> layerBlendingColor, so merging it // before the base pipeline (layerBlendingColor -> panel) yields newLayerBlendColor -> panel const ColorPipeline combined = pipeline.merged(m_baseCrtcColorPipeline); m_pipeline->setCrtcColorPipeline(combined); if (!DrmPipeline::commitPipelines({m_pipeline}, m_gpu, DrmPipeline::CommitMode::Test)) { // restore the base and let the caller fall back m_pipeline->setCrtcColorPipeline(m_baseCrtcColorPipeline); m_pipeline->applyPendingChanges(); return false; } m_pipeline->applyPendingChanges(); State next = m_state; next.layerBlendingColor = newLayerBlendColor; setState(next); m_appliedPostBlendPipeline = pipeline; m_appliedLayerBlendingColor = newLayerBlendColor; return true; } void DrmOutput::resetPostBlendPipeline() { if (!m_appliedLayerBlendingColor) { return; } m_pipeline->setCrtcColorPipeline(m_baseCrtcColorPipeline); m_pipeline->applyPendingChanges(); State next = m_state; next.layerBlendingColor = m_baseLayerBlendingColor; setState(next); m_appliedPostBlendPipeline = ColorPipeline{}; m_appliedLayerBlendingColor = nullptr; } void DrmOutput::maybeScheduleRepaints(const State &next) { // TODO move the output layers to BackendOutput, and have it take care of this when updating State if (next.blendingColor != m_state.blendingColor || next.layerBlendingColor != m_state.layerBlendingColor) { const auto layers = m_pipeline->layers(); for (const auto &layer : layers) { layer->addDeviceRepaint(Region::infinite()); } } } bool DrmOutput::needsShadowBuffer() const { return m_needsShadowBuffer; } void DrmOutput::removePipeline() { m_pipeline = nullptr; } void DrmOutput::setAutoRotateAvailable(bool isAvailable) { m_autoRotateAvailable = isAvailable; Information next = m_information; next.capabilities = computeCapabilities(); setInformation(next); } void DrmOutput::maybeUpdateDpmsState() { // this is only needed for updating the state from "TurningOff" to "Off" if (m_state.dpmsMode == DpmsMode::TurningOff && !m_pipeline->activePending()) { State next = m_state; next.dpmsMode = DpmsMode::Off; setState(next); } } void DrmOutput::setAutoBrightnessAvailable(bool isAvailable) { m_autoBrightnessAvailable = isAvailable; Information next = m_information; next.capabilities = computeCapabilities(); setInformation(next); } const BackendOutput::State &DrmOutput::nextState() const { return m_nextState ? *m_nextState : m_state; } Colorimetry DrmOutput::wireColor(const State &next) const { if (next.wideColorGamut && (capabilities() & Capability::WideColorGamut)) { return Colorimetry::BT2020; } else { return next.originalColorDescription->containerColorimetry(); } } TransferFunction::Type DrmOutput::wireTransfer(const State &next) const { if (next.highDynamicRange && (capabilities() & Capability::HighDynamicRange)) { return TransferFunction::Type::PerceptualQuantizer; } else { return TransferFunction::Type::gamma22; } } } #include "moc_drm_output.cpp"