diff --git a/drivers/gpu/drm/apple/dcp-edr.h b/drivers/gpu/drm/apple/dcp-edr.h new file mode 100644 index 0000000000000..462f3076bee2d --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-edr.h @@ -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 +#include +#include +#include + +/* + * 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__ */ diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index f27b6cfefb882..1064f68849d44 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -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 { @@ -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; diff --git a/drivers/gpu/drm/apple/dcp-panel-adt.c b/drivers/gpu/drm/apple/dcp-panel-adt.c index 5887091aa57cf..521ff6cd79a3a 100644 --- a/drivers/gpu/drm/apple/dcp-panel-adt.c +++ b/drivers/gpu/drm/apple/dcp-panel-adt.c @@ -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); @@ -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++) { @@ -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. */ diff --git a/drivers/gpu/drm/apple/dcp-panel-adt.h b/drivers/gpu/drm/apple/dcp-panel-adt.h index 2d724adb7d0e1..32b585660974a 100644 --- a/drivers/gpu/drm/apple/dcp-panel-adt.h +++ b/drivers/gpu/drm/apple/dcp-panel-adt.h @@ -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; }; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 85caa87494ae7..c5d05af2f976f 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -51,6 +51,7 @@ #include "afk.h" #include "av.h" #include "dcp.h" +#include "dcp-edr.h" #include "dcp-panel-adt.h" #include #include @@ -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. */ diff --git a/drivers/gpu/drm/apple/dcp_backlight_test.c b/drivers/gpu/drm/apple/dcp_backlight_test.c index c8a4d31bde2af..2a40415fb635e 100644 --- a/drivers/gpu/drm/apple/dcp_backlight_test.c +++ b/drivers/gpu/drm/apple/dcp_backlight_test.c @@ -5,6 +5,7 @@ #include #include "dcp_backlight.h" +#include "dcp-edr.h" static void backlight_millinits_takeover_test(struct kunit *test) { @@ -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), @@ -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), {} }; diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 2d7268f84e290..eadd480e76690 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -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" @@ -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 { \ @@ -184,7 +187,8 @@ 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; @@ -192,10 +196,13 @@ static void dcp_edr_prepare(struct apple_dcp *dcp, struct DCP_FW_NAME(dcp_swap) 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; @@ -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) { diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 860578ee16f1a..7eeef292efaf0 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -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')) @@ -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