From 9cd46b2bac71c0e40b5af2288fe4d3955f4399a1 Mon Sep 17 00:00:00 2001 From: garymeyerca <220751193+garymeyerca@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:24:48 -0400 Subject: [PATCH] drm/apple: power off the J613 panel during DPMS and suspend On the M3 MacBook Air, the 14.7 IOMFB power-off path presents a black frame but keeps the panel powered. The backlight therefore remains lit through DPMS off and system suspend. Enable panel power handling in the J613 board record. Complete the final black frame before A472(0), and send A475(1) before A472(1) when enabling the panel again. Handle the accompanying power callbacks in the internal callback dispatcher and invalidate the cached colour matrix on wake. Track acknowledged panel power state, reject swaps while powered off, and preserve state when the firmware refuses a transition. Add a standalone host regression for the production power functions and internal callback dispatch, including wire layouts and failure paths. Validate DPMS off/on and two s2idle cycles on J613 with 14.7 firmware. The power sequence and callback layouts follow the existing J613 clamshell work linked below. Link: https://github.com/omacom/linux-aurora/pull/127 Assisted-by: Codex:GPT-6 --- drivers/gpu/drm/apple/iomfb_v14_7.c | 83 +++++- drivers/gpu/drm/apple/iomfb_v14_7_board.h | 2 + tools/testing/apple/check-v14-panel-power.py | 272 +++++++++++++++++++ 3 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 tools/testing/apple/check-v14-panel-power.py diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 0e39ab4b7478e..2a255f58a41f5 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -89,6 +89,7 @@ static const struct dcp_v14_board dcp_v14_board_j613 = { .panel_width = 2560, .panel_height = 1664, .promotion = false, + .panel_power = true, .ctm_set = A(421), .ctm_get = A(420), }; @@ -187,6 +188,8 @@ struct apple_dcp_v14 { struct work_struct stopped_work; bool failed; bool started; + /* The panel acknowledged A472(0); no swaps until A472(1) succeeds. */ + bool panel_off; /* * An external processor: no panel. Its modes come from the @@ -896,6 +899,15 @@ static int dcp_v14_callback(void *cookie, u32 tag, const void *input, u32 in_siz } #define SHAPE(i, o) (in_size == (i) && out_size == (o)) + /* Notifications sent by J613 panel power transitions, also on wake. */ + if (v14->board && v14->board->panel_power) { + if ((tag == D(2) && SHAPE(0, 0)) || + (tag == D(591) && SHAPE(20, 0)) || + (tag == D(593) && SHAPE(4, 0))) + return 0; + if (tag == D(574) && SHAPE(4, 4)) + return in[0] > 1 ? -EINVAL : 0; + } /* * Backlight correction notification: one signed 32-bit factor, no * reply payload. The firmware applies it when the colour matrix @@ -2519,6 +2531,11 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, return -ENOMEM; mutex_lock(&v14->lock); + if (v14->panel_off) { + mutex_unlock(&v14->lock); + kfree(swap); + return -EAGAIN; + } admission.generation = v14->description_generation; /* * A colour matrix the firmware refuses costs night light, not the @@ -2580,6 +2597,12 @@ static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *f ret = dcp_v14_swap(v14, fb ? surface : NULL, iova, fb ? width : 0, fb ? height : 0, dst_y, ctm_state); elapsed = ktime_get_ns() - start; + /* A powered-off panel took no frame: the old framebuffer stays pinned. */ + if (ret == -EAGAIN && !v14->external && !READ_ONCE(v14->failed)) { + if (fb) + drm_framebuffer_put(fb); + return false; + } if (ret && v14->external && !READ_ONCE(v14->failed)) { /* Refused before it was taken: the old framebuffer stays on screen. */ if (fb) @@ -2907,14 +2930,56 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, dcp_v14_cancel_event(dcp); } +/* + * The boot loader leaves the panel on. Keep the old state on a refused + * transition, so a later enable can retry. A CRTC enable requests the internal + * panel: A475(1) must precede A472(1), otherwise the firmware's uninitialised + * lid state can select the external route instead. The coprocessor stays on. + * Called with the RPC lock held, after the final black frame on power-off. + */ +static int dcp_v14_panel_set_power(struct apple_dcp_v14 *v14, bool on) +{ + u8 power[12] = {}, reply[8] = {}, panel[4] = { 1 }; + int ret; + + if (v14->panel_off == !on) + return 0; + if (on) { + ret = dcp_v14_call(v14, A(475), panel, sizeof(panel), NULL, 0, 0); + if (ret) + return ret; + } + power[0] = on; + ret = dcp_v14_call(v14, A(472), power, sizeof(power), reply, sizeof(reply), 0); + if (ret) + return ret; + if (get_unaligned_le32(reply + 4)) + return -EIO; + v14->panel_off = !on; + return 0; +} + void iomfb_v14_7_poweron(struct apple_dcp *dcp) { - /* The panel stays on; the next swap shows the plane again. */ + struct apple_dcp_v14 *v14 = dcp->v14; + int ret; + + if (!v14 || !v14->started || !v14->board || !v14->board->panel_power) + return; + mutex_lock(&v14->lock); + ret = v14->failed ? -EIO : + dcp_v14_panel_set_power(v14, true); + if (!ret) + v14->ctm_valid = false; + mutex_unlock(&v14->lock); + if (ret) + dev_warn(dcp->dev, "panel power-on failed: %d\n", ret); } void iomfb_v14_7_poweroff(struct apple_dcp *dcp) { struct apple_dcp_v14 *v14 = dcp->v14; + int ret; /* An external display is powered down; the next enable sets its mode again. */ if (dcp->external) { @@ -2927,7 +2992,17 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) dcp_mode_invalidate(&dcp->mode_state); return; } - /* Blank to black; the panel and the DCP stay powered. */ - if (v14 && v14->started) - dcp_v14_present(v14, NULL, 0, 0, 0, NULL); + if (!v14 || !v14->started || READ_ONCE(v14->panel_off)) + return; + /* Complete the black frame before powering off or releasing its surface. */ + if (!dcp_v14_present(v14, NULL, 0, 0, 0, NULL)) + return; + if (!v14->board || !v14->board->panel_power) + return; + mutex_lock(&v14->lock); + ret = v14->failed ? -EIO : + dcp_v14_panel_set_power(v14, false); + mutex_unlock(&v14->lock); + if (ret) + dev_warn(dcp->dev, "panel power-off failed: %d\n", ret); } diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_board.h b/drivers/gpu/drm/apple/iomfb_v14_7_board.h index 368dab49c2944..a236a6031acf7 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_board.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7_board.h @@ -27,6 +27,8 @@ struct dcp_v14_board { u32 panel_width, panel_height; /* The panel may have a 120 Hz timing besides 60 Hz. */ bool promotion; + /* A472 panel power and its resume callbacks are known on this board. */ + bool panel_power; /* * Log what the first boots of a new board are judged by, not only with * debugging on: every panel timing the firmware offers, and the display diff --git a/tools/testing/apple/check-v14-panel-power.py b/tools/testing/apple/check-v14-panel-power.py new file mode 100644 index 0000000000000..8db9b3969d721 --- /dev/null +++ b/tools/testing/apple/check-v14-panel-power.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise the production 14.7 panel power sequence without display hardware.""" + +import argparse +import pathlib +import re +import subprocess +import tempfile + +ROOT = pathlib.Path(__file__).resolve().parents[3] +TEST = r''' +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint32_t u32; +#define A(n) (n) +#define D(n) (n) +#define READ_ONCE(x) (x) +#define mutex_lock(x) ((void)(x)) +#define mutex_unlock(x) ((void)(x)) +#define dev_warn(...) ((void)0) +struct dcp_v14_board { bool panel_power; }; +struct apple_dcp_v14 { + const struct dcp_v14_board *board; + bool started, failed, panel_off, ctm_valid, opened, powered, external; + int lock; +}; +struct apple_dcp { struct apple_dcp_v14 *v14; bool external; void *dev; int mode_state; }; +static u32 get_unaligned_le32(const void *p) +{ + const u8 *b = p; + return (u32)b[0] | (u32)b[1] << 8 | (u32)b[2] << 16 | (u32)b[3] << 24; +} + +struct firmware { + unsigned int calls, method[4], refuse_method; + int transport; + bool on; +}; + +static struct firmware firmware; +static bool black_ok = true; +static unsigned int black_frames; +static int dcp_v14_call(struct apple_dcp_v14 *v14, unsigned int method, + const void *in, u32 in_size, void *out, u32 out_size, + u32 completion) +{ + struct firmware *fw = &firmware; + const u8 *bytes = in; + unsigned int i; + + (void)v14; + assert(completion == 0); + + assert(fw->calls < 4); + fw->method[fw->calls++] = method; + if (method == 475) { + assert(in_size == 4 && out_size == 0 && !out); + assert(bytes[0] == 1); + } else { + assert(method == 472); + assert(in_size == 12 && out_size == 8 && out); + assert(bytes[0] == fw->on); + } + for (i = 1; i < in_size; i++) + assert(bytes[i] == 0); + if (method == fw->refuse_method) { + if (fw->transport) + return fw->transport; + assert(method == 472); + ((u8 *)out)[4] = 1; + } + return 0; +} + +static bool dcp_v14_present(struct apple_dcp_v14 *v14, void *fb, + u32 width, u32 height, u32 y, void *ctm) +{ + assert(!fb && !width && !height && !y && !ctm); + assert(!v14->panel_off); + black_frames++; + return black_ok; +} +/* The external branch is outside this regression; it must never run here. */ +static int dcp_v14_set_power(struct apple_dcp_v14 *v14, bool on) +{ + (void)v14; + (void)on; + assert(0); + return 0; +} +static void dcp_mode_invalidate(void *state) { (void)state; assert(0); } +@@CALLBACK@@ +@@FUNCTIONS@@ + +static void power_callbacks(void) +{ + struct dcp_v14_board board = {.panel_power = true}; + struct apple_dcp_v14 v14 = {.board = &board}; + u8 in[20] = {}, out[4] = {}; + + assert(dcp_v14_callback(&v14, D(2), NULL, 0, NULL, 0) == 0); + assert(dcp_v14_callback(&v14, D(591), in, 20, NULL, 0) == 0); + assert(dcp_v14_callback(&v14, D(593), in, 4, NULL, 0) == 0); + assert(dcp_v14_callback(&v14, D(574), in, 4, out, 4) == 0); + in[0] = 1; + assert(dcp_v14_callback(&v14, D(574), in, 4, out, 4) == 0); + in[0] = 2; + assert(dcp_v14_callback(&v14, D(574), in, 4, out, 4) == -EINVAL); + assert(dcp_v14_callback(&v14, D(2), in, 4, NULL, 0) == -ENOENT); + board.panel_power = false; + assert(dcp_v14_callback(&v14, D(2), NULL, 0, NULL, 0) == -ENOENT); +} + +static void sleep_wake(void) +{ + const struct dcp_v14_board board = {.panel_power = true}; + struct apple_dcp_v14 v14 = {.board = &board, .started = true, .ctm_valid = true}; + struct apple_dcp dcp = {.v14 = &v14}; + + firmware = (struct firmware){0}; + black_frames = 0; + iomfb_v14_7_poweron(&dcp); + assert(!firmware.calls && !v14.panel_off); + iomfb_v14_7_poweroff(&dcp); + assert(v14.panel_off && firmware.calls == 1 && black_frames == 1); + assert(firmware.method[0] == 472); + iomfb_v14_7_poweroff(&dcp); + assert(firmware.calls == 1 && black_frames == 1); + firmware.on = true; + v14.ctm_valid = true; + iomfb_v14_7_poweron(&dcp); + assert(!v14.panel_off && !v14.ctm_valid && firmware.calls == 3); + assert(firmware.method[1] == 475 && firmware.method[2] == 472); + iomfb_v14_7_poweron(&dcp); + assert(firmware.calls == 3); +} + +static void refused_transition(bool on, int error) +{ + struct apple_dcp_v14 v14 = {.panel_off = on}; + firmware = (struct firmware){.on = on, .refuse_method = 472, .transport = error}; + + assert(dcp_v14_panel_set_power(&v14, on) == (error ? error : -EIO)); + assert(v14.panel_off == on); + assert(firmware.calls == (on ? 2 : 1)); + /* A refusal must leave the state retryable, particularly a failed wake. */ + firmware.calls = 0; + firmware.refuse_method = 0; + assert(dcp_v14_panel_set_power(&v14, on) == 0); + assert(v14.panel_off == !on); +} + +static void incomplete_or_unsupported(void) +{ + struct dcp_v14_board board = {.panel_power = true}; + struct apple_dcp_v14 v14 = {.board = &board, .started = true}; + struct apple_dcp dcp = {.v14 = &v14}; + + firmware = (struct firmware){0}; + black_frames = 0; + black_ok = false; + iomfb_v14_7_poweroff(&dcp); + assert(!v14.panel_off && !firmware.calls && black_frames == 1); + black_ok = true; + board.panel_power = false; + iomfb_v14_7_poweroff(&dcp); + assert(!v14.panel_off && !firmware.calls && black_frames == 2); + iomfb_v14_7_poweron(&dcp); + assert(!firmware.calls); + board.panel_power = true; + v14.started = false; + iomfb_v14_7_poweron(&dcp); + iomfb_v14_7_poweroff(&dcp); + assert(!firmware.calls && black_frames == 2); +} + +static void refused_panel_selection(void) +{ + struct apple_dcp_v14 v14 = {.panel_off = true}; + firmware = (struct firmware){.on = true, .refuse_method = 475, .transport = -EIO}; + + assert(dcp_v14_panel_set_power(&v14, true) == -EIO); + assert(v14.panel_off && firmware.calls == 1 && firmware.method[0] == 475); +} + +int main(void) +{ + unsigned int i; + + for (i = 0; i < 100; i++) + sleep_wake(); + refused_transition(false, 0); + refused_transition(true, 0); + refused_transition(false, -110); + refused_transition(true, -110); + refused_panel_selection(); + incomplete_or_unsupported(); + power_callbacks(); + puts("PASS: sleep/wake ordering, idempotence, wire layouts, refusals, " + "transport failures and internal power callbacks"); +} +''' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=pathlib.Path, + default=ROOT / "drivers/gpu/drm/apple/iomfb_v14_7.c") + args = parser.parse_args() + with tempfile.TemporaryDirectory(prefix="v14-panel-power-") as temporary: + scratch = pathlib.Path(temporary) + source = scratch / "test.c" + driver = args.source.read_text() + functions = [] + for name in ("dcp_v14_panel_set_power", "iomfb_v14_7_poweron", "iomfb_v14_7_poweroff"): + match = re.search(r"^\w[\w\s*]*?\b" + name + r"\(", driver, re.M) + assert match, name + start = driver.index("{", match.end()) + end, depth = start + 1, 1 + while depth: + depth += (driver[end] == "{") - (driver[end] == "}") + end += 1 + functions.append(driver[match.start():end]) + # Exercise the real callback entry and dispatch, before unrelated handlers. + # This catches accidentally placing panel handling in the external-only path. + begin = driver.rindex("static int dcp_v14_callback(") + end = driver.index("\t/*\n\t * Backlight correction notification", begin) + callback = driver[begin:end] + callback += r""" + (void)p; + (void)count; + (void)offset; + (void)service; + (void)key_offset; + (void)id; + return -ENOENT; +} +#undef SHAPE +""" + stub = r""" +static int dcp_v14_external_callback(struct apple_dcp_v14 *v, u32 tag, + const u8 *in, u32 a, u8 *out, u32 b) +{ + (void)v; + (void)tag; + (void)in; + (void)a; + (void)out; + (void)b; + assert(0); + return -ENOENT; +} +""" + program = TEST.replace("@@FUNCTIONS@@", "\n".join(functions)) + source.write_text(program.replace("@@CALLBACK@@", stub + callback)) + binary = scratch / "test" + subprocess.run( + ["clang", "-std=gnu11", "-Wall", "-Wextra", "-Werror", + "-fsanitize=address,undefined", "-g", str(source), "-o", str(binary)], + check=True, + ) + subprocess.run([str(binary)], check=True) + + +if __name__ == "__main__": + main()