Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions drivers/gpu/drm/apple/dcp-edr.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* SPDX-License-Identifier: GPL-2.0-only OR MIT */
/* Copyright 2026 Ryan Murray */

#ifndef __APPLE_DCP_EDR_H__
#define __APPLE_DCP_EDR_H__

#include <linux/limits.h>
#include <linux/math64.h>
#include <linux/minmax.h>
#include <linux/types.h>

/*
* EDR headroom on a desktop 26.x panel, x1000: 1000 is SDR white at the
* panel level. A swap asks for brightness SDR white x H and scales the
* picture by 1 / H (see dcp_edr_prepare()).
*/
#define DCP_EDR_UNITY 1000U
#define DCP_EDR_STEP_UP 1047U /* x1000 per swap */

/*
* The panel's potential headroom, the ADT backlight node's
* EDRPotentialHeadroom (16.16), as a ceiling x1000. macOS never gives the
* panel more. 0: no ceiling, for a value that is missing or not above 1.0.
*/
static inline u32 dcp_edr_ceiling(u32 fixed)
{
u64 hm = ((u64)fixed * DCP_EDR_UNITY) >> 16;

return hm > DCP_EDR_UNITY && hm <= U32_MAX ? hm : 0;
}

/* The headroom the panel has above @sdr nits: peak / sdr, up to @ceiling. */
static inline u32 dcp_edr_target(u32 peak, u32 sdr, u32 ceiling)
{
u32 target = DCP_EDR_UNITY;

if (sdr && peak > sdr)
target = div_u64((u64)peak * DCP_EDR_UNITY, sdr);
if (ceiling)
target = clamp(target, DCP_EDR_UNITY, ceiling);
return target;
}

/*
* The headroom for the next swap: up one step per swap, down at once. A
* swap that lowers SDR white from @prev_sdr to @sdr while the headroom is
* below its target raises the headroom just enough to hold the panel level,
* as far as the target allows, and no further: the firmware eases the panel
* level over several frames but applies the scale at once, so a level that
* moved against the fade would show as a dip and a recovery. Held, only the
* scale moves and SDR content dims in the frame that asks for it. A ramp
* cut short continues once SDR white settles.
*/
static inline u32 dcp_edr_next(u32 cur, u32 target, u32 prev_sdr, u32 sdr)
{
if (target < cur)
return target;
if (target == cur)
return cur;
if (sdr && prev_sdr > sdr)
return min_t(u64, div_u64((u64)cur * prev_sdr + sdr / 2, sdr), target);
return min_t(u64, div_u64((u64)cur * DCP_EDR_STEP_UP, DCP_EDR_UNITY) + 1,
target);
}

#endif /* __APPLE_DCP_EDR_H__ */
9 changes: 6 additions & 3 deletions drivers/gpu/drm/apple/dcp-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ struct dcp_panel {
bool has_mini_led;
/// peak panel luminance in nits, for EDR (desktop 26.x; 0 if unknown)
u32 peak_nits;
/// EDR headroom ceiling x1000 from the ADT (desktop 26.x; 0 if none)
u32 edr_ceiling;
};

enum dcp_iomfb_method_profile {
Expand Down Expand Up @@ -594,11 +596,12 @@ struct apple_dcp {
bool output_edr;
bool edr_sent;
/*
* EDR headroom the panel was last given, x1000 (0: not in use), and
* a power off since, after which the ramp starts again from 1.0 (see
* dcp_edr_prepare()).
* EDR headroom the panel was last given, x1000 (0: not in use), the
* SDR white in nits it was given for, and a power off since, after
* which the ramp starts again from 1.0 (see dcp_edr_prepare()).
*/
u32 edr_hm;
u32 edr_sdr;
bool edr_restart;
struct dcp_set_digital_out_mode_req mode;

Expand Down
5 changes: 4 additions & 1 deletion drivers/gpu/drm/apple/dcp-panel-adt.c
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ static int dcp_adt_node(struct dcp_adt_cursor *c, struct dcp_panel_adt *panel,
size_t props_start, props_end;
u32 props, children;
char node_name[33] = {};
bool product, selected;
bool product, selected, backlight;

if (IS_ERR(header))
return PTR_ERR(header);
Expand Down Expand Up @@ -106,6 +106,7 @@ static int dcp_adt_node(struct dcp_adt_cursor *c, struct dcp_panel_adt *panel,
product = depth == 1 && !strcmp(node_name, "product");
selected = (depth == 1 && !strcmp(node_name, "panel")) ||
(product_parent && !strcmp(node_name, "panel"));
backlight = depth == 1 && !strcmp(node_name, "backlight");
/* /product carries raw-panel-serial-number on machines without /panel. */
c->pos = props_start;
for (u32 i = 0; i < props; i++) {
Expand All @@ -115,6 +116,8 @@ static int dcp_adt_node(struct dcp_adt_cursor *c, struct dcp_panel_adt *panel,

if (selected || (product && !strcmp(name, "raw-panel-serial-number")))
dcp_adt_panel_property(panel, name, prop + 36, size);
else if (backlight && !strcmp(name, "EDRPotentialHeadroom") && size == 4)
panel->edr_potential_headroom = get_unaligned_le32(prop + 36);
c->pos += 36 + ALIGN((size_t)size, 4);
}
/* A valid ADT may have neither panel node; retain the OF defaults. */
Expand Down
2 changes: 2 additions & 0 deletions drivers/gpu/drm/apple/dcp-panel-adt.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
struct dcp_panel_adt {
struct dcp_display_metadata identity;
u32 width_mm, height_mm;
/* /backlight EDRPotentialHeadroom, 16.16; 0 if absent */
u32 edr_potential_headroom;
bool found;
};

Expand Down
5 changes: 5 additions & 0 deletions drivers/gpu/drm/apple/dcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
#include "afk.h"
#include "av.h"
#include "dcp.h"
#include "dcp-edr.h"
#include "dcp-panel-adt.h"
#include <linux/io.h>
#include <linux/of_address.h>
Expand Down Expand Up @@ -878,9 +879,13 @@ static void dcp_panel_adt_identity(struct apple_dcp *dcp)
dcp->panel.width_mm = panel.width_mm;
dcp->panel.height_mm = panel.height_mm;
dcp->desktop->panel_adt_identity = panel.identity;
dcp->panel.edr_ceiling = dcp_edr_ceiling(panel.edr_potential_headroom);
dev_info(dcp->dev, "panel identity fallback: ADT %s, panel node %s, dimensions %ux%u mm\n",
ret ? "unavailable" : "parsed", panel.found ? "present" : "absent",
panel.width_mm, panel.height_mm);
if (dcp->panel.edr_ceiling)
dev_info(dcp->dev, "panel EDR headroom up to %u.%03u\n",
dcp->panel.edr_ceiling / 1000, dcp->panel.edr_ceiling % 1000);
}

/* Internal panel identity has no detachable Type-C service or EDID transport. */
Expand Down
132 changes: 132 additions & 0 deletions drivers/gpu/drm/apple/dcp_backlight_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <linux/module.h>

#include "dcp_backlight.h"
#include "dcp-edr.h"

static void backlight_millinits_takeover_test(struct kunit *test)
{
Expand Down Expand Up @@ -633,6 +634,132 @@ static void backlight_reports_keep_target_test(struct kunit *test)
KUNIT_EXPECT_FALSE(test, state.dirty);
}


/* Synthetic panel: a 1200 nit peak and a potential headroom of 8.0. */
#define EDR_TEST_PEAK 1200U
#define EDR_TEST_CEILING 0x80000U

static void edr_ceiling_test(struct kunit *test)
{
KUNIT_EXPECT_EQ(test, dcp_edr_ceiling(0), 0U);
KUNIT_EXPECT_EQ(test, dcp_edr_ceiling(0x8000), 0U);
KUNIT_EXPECT_EQ(test, dcp_edr_ceiling(0x10000), 0U);
KUNIT_EXPECT_EQ(test, dcp_edr_ceiling(0x18000), 1500U);
KUNIT_EXPECT_EQ(test, dcp_edr_ceiling(EDR_TEST_CEILING), 8000U);
KUNIT_EXPECT_EQ(test, dcp_edr_ceiling(U32_MAX), 65535999U);
}

static void edr_target_test(struct kunit *test)
{
u32 ceiling = dcp_edr_ceiling(EDR_TEST_CEILING);

/* Bright SDR white: the peak sets the headroom. */
KUNIT_EXPECT_EQ(test, dcp_edr_target(EDR_TEST_PEAK, 400, ceiling), 3000U);
KUNIT_EXPECT_EQ(test, dcp_edr_target(EDR_TEST_PEAK, 150, ceiling), 8000U);
/* Dim SDR white: the ceiling does, never peak / sdr. */
KUNIT_EXPECT_EQ(test, dcp_edr_target(EDR_TEST_PEAK, 6, ceiling), 8000U);
KUNIT_EXPECT_EQ(test, dcp_edr_target(EDR_TEST_PEAK, 1, ceiling), 8000U);
KUNIT_EXPECT_EQ(test, dcp_edr_target(EDR_TEST_PEAK, 6, 0), 200000U);
/* No headroom above the peak, and none to give with no SDR white. */
KUNIT_EXPECT_EQ(test, dcp_edr_target(EDR_TEST_PEAK, 1200, ceiling), 1000U);
KUNIT_EXPECT_EQ(test, dcp_edr_target(EDR_TEST_PEAK, 1500, ceiling), 1000U);
KUNIT_EXPECT_EQ(test, dcp_edr_target(EDR_TEST_PEAK, 0, ceiling), 1000U);
KUNIT_EXPECT_EQ(test, dcp_edr_target(10000, 1, 0), 10000000U);
}

/*
* Present a fade of SDR white from @from to @to in @steps swaps, as the
* backlight fade does, then @extra swaps at the new level, and return the
* number of swaps whose panel level moved against the fade.
*/
static unsigned int edr_fade(struct kunit *test, u32 *hm, u32 ceiling,
u32 from, u32 to, unsigned int steps,
unsigned int extra, u32 *last_bri)
{
u32 prev = from, bri = (u64)from * *hm / DCP_EDR_UNITY;
unsigned int reversals = 0;

for (unsigned int i = 1; i <= steps + extra; i++) {
u32 sdr = i >= steps ? to :
(from > to ? from - (from - to) * i / steps :
from + (to - from) * i / steps);
u32 target = dcp_edr_target(EDR_TEST_PEAK, sdr, ceiling);
u32 next = dcp_edr_next(*hm, target, prev, sdr);
u32 level = (u64)sdr * next / DCP_EDR_UNITY;

KUNIT_EXPECT_LE(test, next, max(target, *hm));
/* Allow the rounding of the headroom and of the level. */
u32 slack = max(1U, bri / 200);

if ((from > to && level > bri + slack) ||
(from < to && level + slack < bri))
reversals++;
*hm = next;
prev = sdr;
bri = level;
}
*last_bri = bri;
return reversals;
}

static void edr_low_end_steady_test(struct kunit *test)
{
u32 ceiling = dcp_edr_ceiling(EDR_TEST_CEILING);
u32 hm = dcp_edr_target(EDR_TEST_PEAK, 35, ceiling), bri;

/* At the ceiling below 150 nits: a fade only moves the panel level. */
KUNIT_EXPECT_EQ(test, edr_fade(test, &hm, ceiling, 35, 6, 15, 0, &bri), 0U);
KUNIT_EXPECT_EQ(test, hm, 8000U);
KUNIT_EXPECT_EQ(test, bri, 48U);
/* Swaps at the floor, e.g. for an OSD, change nothing. */
for (unsigned int i = 0; i < 200; i++)
KUNIT_EXPECT_EQ(test, dcp_edr_next(hm, dcp_edr_target(EDR_TEST_PEAK, 6, ceiling),
6, 6), hm);
}

static void edr_fade_monotonic_test(struct kunit *test)
{
u32 ceilings[] = { dcp_edr_ceiling(EDR_TEST_CEILING), 0 };

for (unsigned int c = 0; c < ARRAY_SIZE(ceilings); c++) {
u32 ceiling = ceilings[c];
u32 hm = dcp_edr_target(EDR_TEST_PEAK, 400, ceiling), bri;

/* Down: the panel level holds, then follows SDR white down. */
KUNIT_EXPECT_EQ(test, edr_fade(test, &hm, ceiling, 400, 6, 15, 300, &bri), 0U);
KUNIT_EXPECT_EQ(test, hm, dcp_edr_target(EDR_TEST_PEAK, 6, ceiling));
KUNIT_EXPECT_EQ(test, bri, (u32)((u64)6 * hm / DCP_EDR_UNITY));
/* Up: the headroom drops at once and the level only rises. */
KUNIT_EXPECT_EQ(test, edr_fade(test, &hm, ceiling, 6, 400, 15, 10, &bri), 0U);
KUNIT_EXPECT_EQ(test, hm, 3000U);
KUNIT_EXPECT_EQ(test, bri, EDR_TEST_PEAK);
/* One step at a time between near levels. */
KUNIT_EXPECT_EQ(test, edr_fade(test, &hm, ceiling, 400, 300, 15, 50, &bri), 0U);
KUNIT_EXPECT_EQ(test, hm, 4000U);
}
}

static void edr_start_ramp_test(struct kunit *test)
{
u32 ceiling = dcp_edr_ceiling(EDR_TEST_CEILING);
u32 target = dcp_edr_target(EDR_TEST_PEAK, 6, ceiling);
u32 hm = DCP_EDR_UNITY;
unsigned int swaps = 0;

/* EDR starting at a steady level rises one step per swap. */
while (hm < target) {
u32 next = dcp_edr_next(hm, target, 6, 6);

KUNIT_ASSERT_GT(test, next, hm);
KUNIT_EXPECT_LE(test, next, hm * DCP_EDR_STEP_UP / DCP_EDR_UNITY + 1);
hm = next;
KUNIT_ASSERT_LT(test, ++swaps, 100U);
}
KUNIT_EXPECT_EQ(test, hm, target);
/* Ending EDR returns to 1.0 in one swap. */
KUNIT_EXPECT_EQ(test, dcp_edr_next(hm, DCP_EDR_UNITY, 6, 6), DCP_EDR_UNITY);
}

static struct kunit_case backlight_cases[] = {
KUNIT_CASE(backlight_reports_keep_target_test),
KUNIT_CASE(backlight_millinits_takeover_test),
Expand All @@ -654,6 +781,11 @@ static struct kunit_case backlight_cases[] = {
KUNIT_CASE(backlight_fade_test),
KUNIT_CASE(backlight_fade_restart_test),
KUNIT_CASE(backlight_fade_blank_test),
KUNIT_CASE(edr_ceiling_test),
KUNIT_CASE(edr_target_test),
KUNIT_CASE(edr_low_end_steady_test),
KUNIT_CASE(edr_fade_monotonic_test),
KUNIT_CASE(edr_start_ramp_test),
{}
};

Expand Down
27 changes: 15 additions & 12 deletions drivers/gpu/drm/apple/iomfb_template.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

#include "dcp.h"
#include "dcp-internal.h"
#include "dcp-edr.h"
#include "iomfb.h"
#include "iomfb_internal.h"
#include "parser.h"
Expand Down Expand Up @@ -160,10 +161,12 @@ static u64 dcp_binary64_ratio(u64 num, u64 den)
* PQ 100 lands at SDR white and highlights reach the panel peak. H rises
* in steps linear in stops, one per swap, about 1 s when EDR starts, with
* a brightness limit of 1.3 x the brightness in the middle of the ramp.
* H never exceeds the panel's potential headroom from the ADT, as on
* macOS: below the SDR white at which the peak needs more, H stays at that
* ceiling and the panel level follows SDR white.
* When EDR ends, headroom used drops to 1.0 and H returns to 1.0.
* Unchanged values are left out: the firmware keeps them.
*/
#define DCP_EDR_STEP_UP 1047 /* x1000 per swap */

#define dcp_swap_set_ratio(swap, field, flag, num, den) \
do { \
Expand All @@ -184,18 +187,22 @@ static void dcp_edr_prepare(struct apple_dcp *dcp, struct DCP_FW_NAME(dcp_swap)
{
u32 peak = READ_ONCE(dcp->panel.peak_nits);
u32 sdr = carries ? nits : dcp_backlight_desktop_nits(dcp);
u32 cur = dcp->edr_hm ?: 1000, target = 1000, next, bri;
u32 cur = dcp->edr_hm ?: DCP_EDR_UNITY, target = DCP_EDR_UNITY, next, bri;
u32 prev_sdr = dcp->edr_sdr;
bool restart = dcp->edr_restart;

dcp->edr_restart = false;
if (!dcp->output_edr && !dcp->edr_hm)
return; /* no EDR, and none to wind down */
if (!sdr)
return;
if (dcp->output_edr && peak > sdr)
target = peak * 1000 / sdr;
if (restart && dcp->output_edr)
cur = 1000;
if (dcp->output_edr)
target = dcp_edr_target(peak, sdr, dcp->panel.edr_ceiling);
if (restart && dcp->output_edr) {
cur = DCP_EDR_UNITY;
prev_sdr = sdr;
}
dcp->edr_sdr = sdr;
if (cur == target && !carries && !restart && dcp->edr_sent == dcp->output_edr)
return;

Expand All @@ -205,13 +212,9 @@ static void dcp_edr_prepare(struct apple_dcp *dcp, struct DCP_FW_NAME(dcp_swap)
* no further swap, and a ramp stuck partway leaves SDR content dimmed
* by the scale (headroom used is already 1.0).
*/
next = cur;
if (target > cur)
next = min(cur * DCP_EDR_STEP_UP / 1000 + 1, target);
else if (target < cur)
next = target;
next = dcp_edr_next(cur, target, prev_sdr, sdr);

bri = (u64)sdr * next / 1000;
bri = (u64)sdr * next / DCP_EDR_UNITY;
dcp_swap_set_brightness(swap, bri);
dcp_swap_set_ratio(swap, edr_scale, 2, 1000, next);
if (dcp->output_edr) {
Expand Down
7 changes: 6 additions & 1 deletion tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,8 +765,12 @@ def node(props, children=()):
return bytes(out)+b''.join(children)
panel=node({'product-id':42,'vendor-id':0x5274,'width-mm':345,'height-mm':223,
'serial-number':b'SYNTHETIC-PANEL','name':b'panel\0'})
# A synthetic potential headroom of 6.5; a nested node of that name is no
# panel backlight and must not override it.
backlight=node({'name':b'backlight\0','EDRPotentialHeadroom':0x68000},
[node({'name':b'backlight\0','EDRPotentialHeadroom':0x10000})])
data=node({'name':b'device-tree\0'},[node({'name':b'product\0'},[panel]),
node({'name':b'chosen\0','serial-number':b'PRIVATE-MACHINE'})])
node({'name':b'chosen\0','serial-number':b'PRIVATE-MACHINE'}),backlight])
base=root/'drivers/gpu/drm/apple'
source=(base/'dcp-panel-adt.c').read_text()
source='\n'.join(l for l in source.splitlines() if not l.startswith('#include'))
Expand Down Expand Up @@ -798,6 +802,7 @@ def node(props, children=()):
assert(panel.found && panel.width_mm==345 && panel.height_mm==223);
assert(!strcmp(panel.identity.manufacturer,"TST") && panel.identity.product==42);
assert(panel.identity.serial && panel.identity.serial==dcp_adt_serial((const u8 *)"SYNTHETIC-PANEL",15));
assert(panel.edr_potential_headroom==0x68000);
for(unsigned n=0;n<sizeof(fixture);n++) {
struct dcp_panel_adt old=panel;
assert(dcp_panel_adt_parse(fixture,n,&panel)<0);
Expand Down