backends/drm: artificially create HDR headroom on SDR screens when showing HDR content

wilder/Plasma/6.3
Xaver Hugl 2 years ago
parent 7f4e1ca15b
commit 6a4d97e804
  1. 19
      src/backends/drm/drm_output.cpp
  2. 2
      src/backends/drm/drm_output.h
  3. 26
      src/compositor_wayland.cpp
  4. 5
      src/core/output.cpp
  5. 3
      src/core/output.h
  6. 10
      src/core/renderbackend.cpp
  7. 4
      src/core/renderbackend.h
  8. 5
      src/core/renderlayerdelegate.cpp
  9. 2
      src/core/renderlayerdelegate.h
  10. 10
      src/scene/scene.cpp
  11. 2
      src/scene/scene.h
  12. 30
      src/scene/workspacescene.cpp
  13. 1
      src/scene/workspacescene.h

@ -330,8 +330,8 @@ bool DrmOutput::present(const std::shared_ptr<OutputFrame> &frame)
return false;
}
Q_EMIT outputChange(frame->damage());
if (frame->brightness() && *frame->brightness() != m_state.currentBrightness) {
updateBrightness(*frame->brightness());
if (frame->brightness() != m_state.currentBrightness || (frame->artificialHdrHeadroom() && frame->artificialHdrHeadroom() != m_state.artificialHdrHeadroom)) {
updateBrightness(frame->brightness().value_or(m_state.currentBrightness.value_or(m_state.brightnessSetting)), frame->artificialHdrHeadroom().value_or(m_state.artificialHdrHeadroom));
}
return true;
}
@ -414,9 +414,9 @@ ColorDescription DrmOutput::createColorDescription(const std::shared_ptr<OutputC
const Colorimetry sdrColorimetry = effectiveWcg ? Colorimetry::fromName(NamedColorimetry::BT709).interpolateGamutTo(nativeColorimetry, props->sdrGamutWideness.value_or(m_state.sdrGamutWideness)) : Colorimetry::fromName(NamedColorimetry::BT709);
// TODO the EDID can contain a gamma value, use that when available and colorSource == ColorProfileSource::EDID
const double maxAverageBrightness = effectiveHdr ? props->maxAverageBrightnessOverride.value_or(m_state.maxAverageBrightnessOverride).value_or(m_connector->edid()->desiredMaxFrameAverageLuminance().value_or(m_state.referenceLuminance)) : 200;
const double maxPeakBrightness = effectiveHdr ? props->maxPeakBrightnessOverride.value_or(m_state.maxPeakBrightnessOverride).value_or(m_connector->edid()->desiredMaxLuminance().value_or(800)) : 200;
const double referenceLuminance = effectiveHdr ? props->referenceLuminance.value_or(m_state.referenceLuminance) : maxPeakBrightness;
const auto transferFunction = TransferFunction{effectiveHdr ? TransferFunction::PerceptualQuantizer : TransferFunction::gamma22}.relativeScaledTo(referenceLuminance);
const double maxPeakBrightness = effectiveHdr ? props->maxPeakBrightnessOverride.value_or(m_state.maxPeakBrightnessOverride).value_or(m_connector->edid()->desiredMaxLuminance().value_or(800)) : 200 * m_state.artificialHdrHeadroom;
const double referenceLuminance = effectiveHdr ? props->referenceLuminance.value_or(m_state.referenceLuminance) : 200;
const auto transferFunction = TransferFunction{effectiveHdr ? TransferFunction::PerceptualQuantizer : TransferFunction::gamma22}.relativeScaledTo(referenceLuminance * m_state.artificialHdrHeadroom);
// 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
const double minBrightness = effectiveHdr ? props->minBrightnessOverride.value_or(m_state.minBrightnessOverride).value_or(TransferFunction::defaultMinLuminanceFor(TransferFunction::PerceptualQuantizer)) : transferFunction.minLuminance;
@ -483,21 +483,24 @@ void DrmOutput::applyQueuedChanges(const std::shared_ptr<OutputChangeSet> &props
void DrmOutput::setBrightnessDevice(BrightnessDevice *device)
{
Output::setBrightnessDevice(device);
updateBrightness(m_state.currentBrightness.value_or(m_state.brightnessSetting));
updateBrightness(m_state.currentBrightness.value_or(m_state.brightnessSetting), m_state.artificialHdrHeadroom);
}
void DrmOutput::updateBrightness(double newBrightness)
void DrmOutput::updateBrightness(double newBrightness, double newArtificialHdrHeadroom)
{
if (m_brightnessDevice) {
if (m_state.highDynamicRange) {
m_brightnessDevice->setBrightness(1);
} else {
m_brightnessDevice->setBrightness(newBrightness);
constexpr double minLuminance = 0.04;
const double effectiveBrightness = (minLuminance + newBrightness) * m_state.artificialHdrHeadroom - minLuminance;
m_brightnessDevice->setBrightness(effectiveBrightness);
}
}
State next = m_state;
next.colorDescription = createColorDescription(std::make_shared<OutputChangeSet>(), newBrightness);
next.currentBrightness = newBrightness;
next.artificialHdrHeadroom = newArtificialHdrHeadroom;
setState(next);
tryKmsColorOffloading();
}

@ -81,7 +81,7 @@ private:
Capabilities computeCapabilities() const;
void updateInformation();
void setBrightnessDevice(BrightnessDevice *device) override;
void updateBrightness(double newBrightness);
void updateBrightness(double newBrightness, double newArtificialHdrHeadroom);
void setScanoutColorDescription(const ColorDescription &description);
QList<std::shared_ptr<OutputMode>> getModes() const;

@ -313,6 +313,7 @@ void WaylandCompositor::composite(RenderLoop *renderLoop)
renderLoop->prepareNewFrame();
auto frame = std::make_shared<OutputFrame>(renderLoop, std::chrono::nanoseconds(1'000'000'000'000 / output->refreshRate()));
bool directScanout = false;
std::optional<double> desiredArtificalHdrHeadroom;
// brightness animations should be skipped when
// - the output is new, and we didn't have the output configuration applied yet
@ -339,6 +340,28 @@ void WaylandCompositor::composite(RenderLoop *renderLoop)
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);
@ -405,7 +428,8 @@ void WaylandCompositor::composite(RenderLoop *renderLoop)
framePass(superLayer, frame.get());
if (frame->brightness() && std::abs(*frame->brightness() - output->brightnessSetting()) > 0.001) {
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();
}

@ -795,6 +795,11 @@ std::optional<double> Output::currentBrightness() const
return m_state.currentBrightness;
}
double Output::artificialHdrHeadroom() const
{
return m_state.artificialHdrHeadroom;
}
BrightnessDevice *Output::brightnessDevice() const
{
return m_brightnessDevice;

@ -366,6 +366,7 @@ public:
double brightnessSetting() const;
std::optional<double> currentBrightness() const;
double artificialHdrHeadroom() const;
const ColorDescription &colorDescription() const;
@ -493,6 +494,8 @@ protected:
/// the actually applied brightness level
std::optional<double> currentBrightness;
bool allowSdrSoftwareBrightness = true;
/// how much HDR headroom is created by increasing the backlight beyond the user setting
double artificialHdrHeadroom = 1.0;
};
void setInformation(const Information &information);

@ -159,6 +159,16 @@ void OutputFrame::setBrightness(double brightness)
m_brightness = brightness;
}
std::optional<double> OutputFrame::artificialHdrHeadroom() const
{
return m_artificialHdrHeadroom;
}
void OutputFrame::setArtificialHdrHeadroom(double edr)
{
m_artificialHdrHeadroom = edr;
}
OutputLayer *RenderBackend::cursorLayer(Output *output)
{
return nullptr;

@ -99,6 +99,9 @@ public:
std::optional<double> brightness() const;
void setBrightness(double brightness);
std::optional<double> artificialHdrHeadroom() const;
void setArtificialHdrHeadroom(double edr);
private:
std::optional<RenderTimeSpan> queryRenderTime() const;
@ -113,6 +116,7 @@ private:
std::vector<std::unique_ptr<RenderTimeQuery>> m_renderTimeQueries;
bool m_presented = false;
std::optional<double> m_brightness;
std::optional<double> m_artificialHdrHeadroom;
};
/**

@ -37,4 +37,9 @@ QList<SurfaceItem *> RenderLayerDelegate::scanoutCandidates(ssize_t maxCount) co
return {};
}
double RenderLayerDelegate::desiredHdrHeadroom() const
{
return 1;
}
} // namespace KWin

@ -61,6 +61,8 @@ public:
*/
virtual void paint(const RenderTarget &renderTarget, const QRegion &region) = 0;
virtual double desiredHdrHeadroom() const;
private:
RenderLayer *m_layer = nullptr;
};

@ -44,6 +44,11 @@ void SceneDelegate::paint(const RenderTarget &renderTarget, const QRegion &regio
m_scene->paint(renderTarget, region == infiniteRegion() ? infiniteRegion() : region.translated(viewport().topLeft()));
}
double SceneDelegate::desiredHdrHeadroom() const
{
return m_scene->desiredHdrHeadroom();
}
void SceneDelegate::frame(OutputFrame *frame)
{
m_scene->frame(this, frame);
@ -143,6 +148,11 @@ void Scene::frame(SceneDelegate *delegate, OutputFrame *frame)
{
}
double Scene::desiredHdrHeadroom() const
{
return 1;
}
} // namespace KWin
#include "moc_scene.cpp"

@ -34,6 +34,7 @@ public:
QRegion prePaint() override;
void postPaint() override;
void paint(const RenderTarget &renderTarget, const QRegion &region) override;
double desiredHdrHeadroom() const override;
private:
Scene *m_scene;
@ -86,6 +87,7 @@ public:
virtual void postPaint() = 0;
virtual void paint(const RenderTarget &renderTarget, const QRegion &region) = 0;
virtual void frame(SceneDelegate *delegate, OutputFrame *frame);
virtual double desiredHdrHeadroom() const;
Q_SIGNALS:
void delegateRemoved(SceneDelegate *delegate);

@ -219,6 +219,36 @@ QList<SurfaceItem *> WorkspaceScene::scanoutCandidates(ssize_t maxCount) const
return ret;
}
static double getDesiredHdrHeadroom(Item *item)
{
if (!item->isVisible()) {
return 1;
}
double ret = 1;
const auto children = item->childItems();
for (const auto &child : children) {
ret = std::max(ret, getDesiredHdrHeadroom(child));
}
const auto &color = item->colorDescription();
if (color.maxHdrLuminance() && *color.maxHdrLuminance() > color.referenceLuminance()) {
return std::max(ret, *color.maxHdrLuminance() / color.referenceLuminance());
} else {
return ret;
}
}
double WorkspaceScene::desiredHdrHeadroom() const
{
double maxHeadroom = 1;
for (const auto &item : stacking_order) {
if (!item->window()->isOnOutput(painted_screen)) {
continue;
}
maxHeadroom = std::max(maxHeadroom, getDesiredHdrHeadroom(item));
}
return maxHeadroom;
}
void WorkspaceScene::frame(SceneDelegate *delegate, OutputFrame *frame)
{
if (waylandServer()) {

@ -54,6 +54,7 @@ public:
void postPaint() override;
void paint(const RenderTarget &renderTarget, const QRegion &region) override;
void frame(SceneDelegate *delegate, OutputFrame *frame) override;
double desiredHdrHeadroom() const override;
virtual bool makeOpenGLContextCurrent();
virtual void doneOpenGLContextCurrent();

Loading…
Cancel
Save