From d57301072438b66baa8a64a32f5af3d4f1623e1a Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 1/5] drm/apple: m4: Carry out-of-band calls on stream 1 IOMFB 26.x takes setPowerState (A473) on the out-of-band channel, the older contexts' +4, which the T8132 words carry as stream 1 (bit 10): the AP packet goes at shared-memory offset 0x8000, the firmware answers on the same stream, and the callbacks it makes meanwhile arrive at 0x68000 with bit 10 set. The J713 backend so far admitted stream 0 only. Let a top-level AP call name stream 1, and while it is outstanding, decode incoming words with bit 10 as stream-1 packets. Calls nested in one of its callbacks stay on stream 1, at the next aligned offset after the outstanding packet, as on stream 0. Stream-0 traffic is unchanged. The first user is system sleep. Signed-off-by: Erik Bourget --- drivers/gpu/drm/apple/dcp-m4-protocol.h | 26 ++++++++-- drivers/gpu/drm/apple/dcp-m4-transaction.h | 50 +++++++++++++++---- drivers/gpu/drm/apple/iomfb_m4.c | 20 +++++--- .../selftests/drivers/gpu/apple-dcp/m4.py | 14 ++++++ 4 files changed, 90 insertions(+), 20 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-m4-protocol.h b/drivers/gpu/drm/apple/dcp-m4-protocol.h index 6fccff5625557..3b5c8c29468c3 100644 --- a/drivers/gpu/drm/apple/dcp-m4-protocol.h +++ b/drivers/gpu/drm/apple/dcp-m4-protocol.h @@ -119,16 +119,36 @@ static inline int dcp_m4_decode_word_ext(u64 word, struct dcp_m4_packet *packet) return 0; } +/* + * While an AP call is outstanding on stream 1 (bit 10, the older contexts' + * out-of-band +4), its reply and callbacks come on stream 1 as well, besides + * any stream-0 traffic. + */ +static inline int dcp_m4_decode_word_oob(u64 word, struct dcp_m4_packet *packet) +{ + u8 stream = (word >> 10) & 3; + int ret; + + if (!stream) + return dcp_m4_decode_word(word, packet); + if (stream != 1) + return -EPROTO; + ret = dcp_m4_decode_word(word & ~0x400ULL, packet); + if (!ret) + packet->stream = 1; + return ret; +} + /* * Replies require their pending request record, not a new packet location. - * Only callbacks may name a nonzero stream (external profile); the internal - * decoder never produces one. + * Callbacks may name a nonzero stream (external profile, or stream 1 during + * an out-of-band call); AP calls use stream 0, or stream 1 out of band. */ static inline int dcp_m4_packet_offset(const struct dcp_m4_packet *packet, bool callback) { unsigned int base; - if (packet->reply || (callback ? packet->stream >= 4 : packet->stream) || + if (packet->reply || (callback ? packet->stream >= 4 : packet->stream > 1) || (packet->callback_flag && (!callback || !packet->remote)) || packet->offset % DCP_M4_ALIGNMENT || packet->offset >= DCP_M4_STREAM_SIZE || packet->length < DCP_M4_PACKET_HEADER || diff --git a/drivers/gpu/drm/apple/dcp-m4-transaction.h b/drivers/gpu/drm/apple/dcp-m4-transaction.h index 7736a7a50ddf3..b9d4c25cddd9b 100644 --- a/drivers/gpu/drm/apple/dcp-m4-transaction.h +++ b/drivers/gpu/drm/apple/dcp-m4-transaction.h @@ -32,13 +32,16 @@ struct dcp_m4_transaction { unsigned int callbacks; int error; bool external; /* DCPEXT profile: streams 0..3 and any flagged callback route */ + bool oob; /* an AP call is outstanding on stream 1 */ }; static inline int dcp_m4_transaction_decode(const struct dcp_m4_transaction *state, u64 word, struct dcp_m4_packet *packet) { - return state->external ? dcp_m4_decode_word_ext(word, packet) : - dcp_m4_decode_word(word, packet); + if (state->external) + return dcp_m4_decode_word_ext(word, packet); + return state->oob ? dcp_m4_decode_word_oob(word, packet) : + dcp_m4_decode_word(word, packet); } static inline int dcp_m4_transaction_fail(struct dcp_m4_transaction *state, int error) @@ -59,14 +62,15 @@ static inline bool dcp_m4_tag(u32 tag, u8 direction) } /* Prepare only; the caller publishes with its DMA barrier and mailbox send. */ -static inline int dcp_m4_call_begin(struct dcp_m4_transaction *state, void *rpc, - size_t rpc_size, u32 tag, bool remote, - const void *input, u32 input_size, u32 output_size, - u64 *word) +/* Stream 1 is out of band: top-level calls only, such as setPowerState. */ +static inline int dcp_m4_call_begin_stream(struct dcp_m4_transaction *state, void *rpc, + size_t rpc_size, u32 tag, u8 stream, bool remote, + const void *input, u32 input_size, u32 output_size, + u64 *word) { struct dcp_m4_packet packet = { .tag = tag, .input_size = input_size, .output_size = output_size, - .remote = remote, + .remote = remote, .stream = stream, }; const struct dcp_m4_packet *outer = NULL; u8 *data; @@ -77,6 +81,8 @@ static inline int dcp_m4_call_begin(struct dcp_m4_transaction *state, void *rpc, if (!rpc || rpc_size != DCP_M4_RPC_SIZE || !word || (input_size && !input) || !dcp_m4_tag(tag, 'A') || state->calls >= DCP_M4_CALL_DEPTH) return dcp_m4_transaction_fail(state, -EINVAL); + if (stream > 1 || (stream && (state->calls || state->callbacks || state->external))) + return dcp_m4_transaction_fail(state, -EINVAL); if (state->calls) { struct dcp_m4_call *pending = &state->call[state->calls - 1]; @@ -88,9 +94,16 @@ static inline int dcp_m4_call_begin(struct dcp_m4_transaction *state, void *rpc, if (state->callbacks && remote != state->callback[state->callbacks - 1].packet.remote) return dcp_m4_transaction_fail(state, -EPROTO); - /* AP calls use stream 0; a callback on another stream cannot recurse. */ - if (state->callbacks && state->callback[state->callbacks - 1].packet.stream) - return dcp_m4_transaction_fail(state, -EOPNOTSUPP); + /* + * AP calls use stream 0; a callback on another stream cannot recurse, + * except that calls nested in an out-of-band callback stay out of band. + */ + if (state->callbacks && state->callback[state->callbacks - 1].packet.stream) { + if (!state->oob || state->callback[state->callbacks - 1].packet.stream != 1) + return dcp_m4_transaction_fail(state, -EOPNOTSUPP); + stream = 1; + packet.stream = stream; + } ret = dcp_m4_packet_size(input_size, output_size, &packet.length); if (ret) return dcp_m4_transaction_fail(state, ret); @@ -119,11 +132,22 @@ static inline int dcp_m4_call_begin(struct dcp_m4_transaction *state, void *rpc, state->call[state->calls++] = (struct dcp_m4_call) { .packet = packet, .callbacks = state->callbacks, }; - *word = 2 | (remote ? 0x100 : 0) | ((u64)packet.offset << 16) | + *word = 2 | (remote ? 0x100 : 0) | ((u64)stream << 10) | ((u64)packet.offset << 16) | ((u64)packet.length << 32); + if (stream) + state->oob = true; return 0; } +static inline int dcp_m4_call_begin(struct dcp_m4_transaction *state, void *rpc, + size_t rpc_size, u32 tag, bool remote, + const void *input, u32 input_size, u32 output_size, + u64 *word) +{ + return dcp_m4_call_begin_stream(state, rpc, rpc_size, tag, 0, remote, input, + input_size, output_size, word); +} + /* Firmware may write output, but must not change the owned packet header. */ static inline int dcp_m4_owned_packet(const void *rpc, const struct dcp_m4_packet *owned, bool callback) @@ -160,6 +184,7 @@ static inline int dcp_m4_call_reply(struct dcp_m4_transaction *state, const void return dcp_m4_transaction_fail(state, ret); call = &state->call[state->calls - 1]; if (!reply.reply || reply.remote != call->packet.remote || + reply.stream != call->packet.stream || state->callbacks != call->callbacks || output_size != call->packet.output_size || (output_size && !output)) return dcp_m4_transaction_fail(state, -EPROTO); @@ -169,6 +194,9 @@ static inline int dcp_m4_call_reply(struct dcp_m4_transaction *state, const void if (output_size) memcpy(output, (const u8 *)rpc + offset + DCP_M4_PACKET_HEADER + call->packet.input_size, output_size); + /* The out-of-band call itself, not a call nested in its callbacks. */ + if (call->packet.stream && state->calls == 1) + state->oob = false; state->calls--; return 0; } diff --git a/drivers/gpu/drm/apple/iomfb_m4.c b/drivers/gpu/drm/apple/iomfb_m4.c index ac68f001626aa..0940bb01a858f 100644 --- a/drivers/gpu/drm/apple/iomfb_m4.c +++ b/drivers/gpu/drm/apple/iomfb_m4.c @@ -650,10 +650,10 @@ static int dcp_m4_runtime_callback(struct dcp_m4_runtime *r, u64 word) return ret; } -static int dcp_m4_runtime_call(void *cookie, unsigned int method, bool remote, - const void *input, u32 input_size, void *output, u32 output_size) +static int dcp_m4_runtime_call_stream(struct dcp_m4_runtime *r, unsigned int method, + u8 stream, bool remote, const void *input, + u32 input_size, void *output, u32 output_size) { - struct dcp_m4_runtime *r = cookie; struct iomfb_m4 *dcp = r->dcp; unsigned long previous = r->deadline; struct dcp_m4_packet message; @@ -671,8 +671,9 @@ static int dcp_m4_runtime_call(void *cookie, unsigned int method, bool remote, } tag = 'A' | (('0' + method / 100) << 8) | (('0' + method / 10 % 10) << 16) | (('0' + method % 10) << 24); - ret = dcp_m4_call_begin(&dcp->transaction, dcp->rpc, DCP_M4_RPC_SIZE, - tag, remote, input, input_size, output_size, &word); + ret = dcp_m4_call_begin_stream(&dcp->transaction, dcp->rpc, DCP_M4_RPC_SIZE, + tag, stream, remote, input, input_size, output_size, + &word); if (!ret) ret = dcp_m4_runtime_send(r, word); while (!ret) { @@ -680,7 +681,7 @@ static int dcp_m4_runtime_call(void *cookie, unsigned int method, bool remote, if (ret) break; dma_rmb(); - ret = dcp_m4_decode_word(word, &message); + ret = dcp_m4_transaction_decode(&dcp->transaction, word, &message); if (ret) { dcp_m4_runtime_bad_word(r, word, ret); break; @@ -702,6 +703,13 @@ static int dcp_m4_runtime_call(void *cookie, unsigned int method, bool remote, return ret; } +static int dcp_m4_runtime_call(void *cookie, unsigned int method, bool remote, + const void *input, u32 input_size, void *output, u32 output_size) +{ + return dcp_m4_runtime_call_stream(cookie, method, 0, remote, input, input_size, + output, output_size); +} + /* One bounded drain; a positive return asks the idle worker to reschedule. */ static int dcp_m4_runtime_drain(struct dcp_m4_runtime *r, unsigned int budget) { diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py index f11078c87b087..4a6c933743a63 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py @@ -196,6 +196,20 @@ def protocol(root): assert(!dcp_m4_callback_ack(&packet, &ack)); assert(ack == (0x342U | (stream << 10))); } + /* Out of band: stream 1 for the AP packet, its reply and its callbacks. */ + assert(dcp_m4_decode_word(0x402 | (12ULL << 32), &packet) == -EPROTO); + assert(!dcp_m4_decode_word_oob(0x402 | (64ULL << 16) | (36ULL << 32), &packet)); + assert(packet.stream == 1 && packet.offset == 64 && !packet.reply); + assert(dcp_m4_packet_offset(&packet, false) == 0x8040); + assert(dcp_m4_packet_offset(&packet, true) == 0x68040); + assert(!dcp_m4_callback_ack(&packet, &ack) && ack == 0x442); + assert(!dcp_m4_nested_offset(&packet, false, 12, &offset) && offset == 128); + assert(!dcp_m4_decode_word_oob(0x442, &packet) && packet.reply && packet.stream == 1); + assert(!dcp_m4_decode_word_oob(2 | (12ULL << 32), &packet) && !packet.stream); + assert(dcp_m4_decode_word_oob(0x802 | (12ULL << 32), &packet) == -EPROTO); + assert(dcp_m4_decode_word_oob(0xc02 | (12ULL << 32), &packet) == -EPROTO); + packet = (struct dcp_m4_packet){.stream = 2, .length = 12}; + assert(dcp_m4_packet_offset(&packet, false) == -EINVAL); puts("PASS READY identity, bounded packet parsing, nested offset and callback routing"); } From c1967d32dc57c93cb99fc0dc7ce9de7ccac4bcbe Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 2/5] drm/apple: m4: Answer the callbacks of setPowerState Powering the J713 display down and up with setPowerState (A473), the 26.6.2 firmware calls back into the AP: acknowledgeSetPowerState (D106), will_power_off_signal (D002), get_calendar_time_ms (D209), powerUpDART (D572), swap_complete_intent (D592), set_kernel_power_assert (D700), aot_supported_peek (D203), set_power_trace_enable (D008), set_idle_caching_state_ap (D117) and enable_backlight_message_ap_gated (D594). The exact-shape table refused them, which quarantines the session. Add their shapes and answer them for this machine: the notifications and setters with an ACK, the status-returning calls with success, AOT support with false, and the calendar time in milliseconds. The DART and the power assertion are the AP's to hold: the J713 keeps the display domains on across suspend-to-idle. Signed-off-by: Erik Bourget --- drivers/gpu/drm/apple/dcp-m4-callback.h | 27 ++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-m4-callback.h b/drivers/gpu/drm/apple/dcp-m4-callback.h index 08249a9f255d0..738f803835469 100644 --- a/drivers/gpu/drm/apple/dcp-m4-callback.h +++ b/drivers/gpu/drm/apple/dcp-m4-callback.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include "dcp-m4-profile.h" @@ -33,16 +34,17 @@ static inline int dcp_m4_callback_result(int ret) /* Exact qualified J713 shapes. An absent behavior hook is failure, never an ACK. */ static const struct dcp_m4_callback_shape dcp_m4_callbacks[] = { - {0, 0, 4}, {1, 0, 4}, {3, 4, 60}, {6, 84, 80}, - {100, 0, 0}, {101, 0, 4}, {102, 68, 0}, {104, 68, 0}, {107, 64, 0}, + {0, 0, 4}, {1, 0, 4}, {2, 0, 0}, {3, 4, 60}, {6, 84, 80}, {8, 4, 0}, + {100, 0, 0}, {101, 0, 4}, {102, 68, 0}, {104, 68, 0}, {106, 0, 4}, {107, 64, 0}, {108, 0, 4}, {109, 0, 4}, {110, 0, 4}, {111, 0, 4}, {112, 0, 4}, {113, 0, 4}, - {114, 4164, 4100}, {121, 0, 4}, {122, 0, 4}, {123, 0, 4}, + {114, 4164, 4100}, {117, 12, 0}, {121, 0, 4}, {122, 0, 4}, {123, 0, 4}, {125, 100, 36}, {127, 4, 4}, {128, 4104, 4}, {129, 64, 4}, - {201, 12, 16}, {206, 0, 4}, {207, 0, 4}, {300, 16, 0}, + {201, 12, 16}, {203, 0, 4}, {206, 0, 4}, {207, 0, 4}, {209, 0, 8}, {300, 16, 0}, {400, 76, 3076}, {401, 80, 12}, {406, 72, 0}, {408, 8, 8}, {413, 4168, 4}, {414, 80, 4}, {415, 76, 4}, {451, 20, 28}, {454, 4, 4}, {552, 4164, 4}, {561, 4164, 4}, {563, 76, 4}, {565, 72, 4}, {567, 128, 4}, - {575, 88, 76}, {576, 4, 0}, {582, 8, 4}, {588, 0, 0}, {590, 1840, 0}, {599, 0, 0}, + {572, 4, 4}, {575, 88, 76}, {576, 4, 0}, {582, 8, 4}, {588, 0, 0}, {590, 1840, 0}, + {592, 20, 0}, {594, 4, 0}, {599, 0, 0}, {700, 4, 4}, }; static inline int dcp_m4_callback_method(u32 tag) @@ -136,6 +138,21 @@ static inline int dcp_m4_callback_dispatch(const struct dcp_m4_callback_ops *ops return 0; case 300: case 406: case 576: case 588: return 0; + /* + * will_power_off_signal, set_power_trace_enable, set_idle_caching_state_ap, + * swap_complete_intent, enable_backlight_message_ap_gated: notifications. + */ + case 2: case 8: case 117: case 592: case 594: + return 0; + /* + * acknowledgeSetPowerState, powerUpDART, set_kernel_power_assert: success; + * aot_supported_peek: false. + */ + case 106: case 572: case 700: case 203: + return 0; + case 209: /* get_calendar_time_ms */ + put_unaligned_le64(ktime_get_real_ns() / NSEC_PER_MSEC, out); + return 0; case 122: case 123: case 582: return 0; /* False, not an unimplemented service returning success. */ case 101: From 53899593311d1e6fd10e7ba91861b970cad63419 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 3/5] drm/apple: m4: Power the display down for system sleep The J713 DCP refused system sleep: the retained T8132 session had no qualified handshake. setPowerState (A473) out of band is that handshake on IOMFB 26.x: with the panel blanked, setPowerState(0) powers the display pipeline down (the firmware logs "power on -> off" and disables its M3 helpers) and setPowerState(1) brings it back, after which the next present scans out again. apple-drm is a consumer of the DCP, so its suspend handler has blanked the panel when the DCP suspends, and its resume handler restores the CRTC after the DCP resumed. Power the display down in between, with the coprocessor running and its power domains kept on through the wakeup path, as on the older DCPs; refuse sleep should the panel still scan out. Presents while the display is down are refused. The request is the 16-byte {u64 state; u8 flags[4]; u8 result_null; u8 pad[3]}, the reply {u32 value; u32 status}; a nonzero status fails the transition and the session. Signed-off-by: Erik Bourget [eryk: keep hibernation refused, as before: SYSTEM_SLEEP_PM_OPS also made this the freeze and poweroff handler, so a blanked J713 could hibernate and restore a session iBoot had restarted; and count the startup surface, shown before the first frame, as scanning out] Signed-off-by: Eryk Wieliczko Co-Authored-By: Claude Opus 5.5 --- drivers/gpu/drm/apple/dcp.c | 34 ++++++-- drivers/gpu/drm/apple/iomfb_m4.c | 81 +++++++++++++++++++ drivers/gpu/drm/apple/iomfb_m4.h | 4 + .../selftests/drivers/gpu/apple-dcp/m4.py | 7 ++ 4 files changed, 121 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 60fb8e561e86a..be07aff95d776 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -6386,9 +6386,17 @@ static int dcp_platform_suspend(struct device *dev) if (dcp_takeover_was_aborted(dcp)) return -EBUSY; - /* The retained T8132 session has no qualified system-sleep handshake. */ - if (iomfb_is_m4(dcp)) - return -EBUSY; + /* + * T8132: setPowerState powers the display down; the coprocessor keeps + * running and its power domains stay on, as below. + */ + if (iomfb_is_m4(dcp)) { + int ret = iomfb_m4_suspend(dcp); + + if (!ret) + device_set_wakeup_path(dev); + return ret; + } if (READ_ONCE(dcp->ext_dead)) return 0; iomfb_v14_7_backlight_suspend(dcp); @@ -6469,7 +6477,7 @@ static int dcp_platform_resume(struct device *dev) return 0; if (iomfb_is_m4(dcp)) - return iomfb_m4_health(dcp); + return iomfb_m4_resume(dcp); if (READ_ONCE(dcp->ext_dead)) return 0; @@ -6492,10 +6500,26 @@ static int dcp_platform_resume(struct device *dev) return 0; } +/* + * Hibernation: the T8132 power-down keeps the retained session, which a + * hibernation image cannot carry across the boot that restores it. + */ +static int dcp_platform_freeze(struct device *dev) +{ + if (iomfb_is_m4(dev_get_drvdata(dev))) + return -EBUSY; + return dcp_platform_suspend(dev); +} + static const struct dev_pm_ops dcp_platform_pm_ops = { .prepare = pm_sleep_ptr(dcp_platform_prepare), .complete = pm_sleep_ptr(dcp_platform_complete), - SYSTEM_SLEEP_PM_OPS(dcp_platform_suspend, dcp_platform_resume) + .suspend = pm_sleep_ptr(dcp_platform_suspend), + .resume = pm_sleep_ptr(dcp_platform_resume), + .freeze = pm_sleep_ptr(dcp_platform_freeze), + .thaw = pm_sleep_ptr(dcp_platform_resume), + .poweroff = pm_sleep_ptr(dcp_platform_freeze), + .restore = pm_sleep_ptr(dcp_platform_resume), }; static const struct apple_dcp_hw_data apple_dcp_hw_t6020 = { diff --git a/drivers/gpu/drm/apple/iomfb_m4.c b/drivers/gpu/drm/apple/iomfb_m4.c index 0940bb01a858f..f30610ee88c90 100644 --- a/drivers/gpu/drm/apple/iomfb_m4.c +++ b/drivers/gpu/drm/apple/iomfb_m4.c @@ -69,6 +69,7 @@ struct iomfb_m4 { size_t allocated; int error; bool started, init_sent, ready, startup_started; + bool powered_off; /* setPowerState(0) for system sleep */ /* Exact completed successor or blank is the only framebuffer retirement. */ struct drm_framebuffer *active_fb; struct drm_framebuffer *uncertain_fb; @@ -1293,6 +1294,8 @@ int iomfb_m4_present(struct apple_dcp *owner, struct drm_framebuffer *fb, return ret; mutex_lock(&dcp->lock); ret = dcp_m4_runtime_live(dcp->runtime); + if (!ret && dcp->powered_off) + ret = -EAGAIN; if (!ret) ret = iomfb_m4_check_frame(&frame); if (ret) @@ -1343,6 +1346,84 @@ int iomfb_m4_blank(struct apple_dcp *owner) return ret; } +/* setPowerState (A473), on the out-of-band stream on IOMFB 26.x. */ +struct dcp_m4_power_state_req { + __le64 state; + u8 flags[4]; + u8 result_null; + u8 padding[3]; +} __packed; +static_assert(sizeof(struct dcp_m4_power_state_req) == 16); + +struct dcp_m4_power_state_reply { + __le32 value; + __le32 status; +} __packed; +static_assert(sizeof(struct dcp_m4_power_state_reply) == 8); + +static int dcp_m4_set_power_state(struct iomfb_m4 *dcp, bool on) +{ + struct dcp_m4_power_state_req req = { .state = cpu_to_le64(on) }; + struct dcp_m4_power_state_reply reply = {}; + int ret; + + lockdep_assert_held(&dcp->lock); + ret = dcp_m4_runtime_live(dcp->runtime); + if (!ret) + ret = dcp_m4_runtime_call_stream(dcp->runtime, 473, 1, false, &req, + sizeof(req), &reply, sizeof(reply)); + if (!ret && reply.status) { + dev_err(dcp->dev, "display power %s refused: status %#x\n", + on ? "on" : "off", le32_to_cpu(reply.status)); + ret = -EIO; + dcp_m4_failed(dcp, ret); + } + return ret; +} + +/* apple-drm, a consumer of this device, has blanked the panel before this. */ +int iomfb_m4_suspend(struct apple_dcp *owner) +{ + struct iomfb_m4 *dcp = owner->v26_6; + int ret; + + if (!dcp || !dcp->runtime) + return -ENODEV; + mutex_lock(&dcp->lock); + /* Also the startup surface, shown before the first frame */ + if (dcp->active_fb || dcp->active_frame.dva) { + dev_warn(dcp->dev, "panel still scanning out: refusing system sleep\n"); + ret = -EBUSY; + } else { + ret = dcp_m4_set_power_state(dcp, false); + } + if (!ret) + dcp->powered_off = true; + mutex_unlock(&dcp->lock); + return ret; +} + +int iomfb_m4_resume(struct apple_dcp *owner) +{ + struct iomfb_m4 *dcp = owner->v26_6; + int ret; + + if (!dcp || !dcp->runtime) + return -ENODEV; + mutex_lock(&dcp->lock); + if (dcp->powered_off) { + ret = dcp_m4_set_power_state(dcp, true); + if (!ret) + dcp->powered_off = false; + } else { + ret = dcp_m4_runtime_health(dcp->runtime); + if (ret) + dcp_m4_failed(dcp, ret); + } + mutex_unlock(&dcp->lock); + return ret; +} + static void iomfb_m4_backlight_work(struct work_struct *work) { struct iomfb_m4 *dcp = container_of(work, struct iomfb_m4, backlight_work); diff --git a/drivers/gpu/drm/apple/iomfb_m4.h b/drivers/gpu/drm/apple/iomfb_m4.h index 29296e527f1d4..f89ddcf0844e8 100644 --- a/drivers/gpu/drm/apple/iomfb_m4.h +++ b/drivers/gpu/drm/apple/iomfb_m4.h @@ -27,6 +27,8 @@ int iomfb_m4_error(struct apple_dcp *dcp); int iomfb_m4_present(struct apple_dcp *dcp, struct drm_framebuffer *fb, dma_addr_t dva, size_t mapped); int iomfb_m4_blank(struct apple_dcp *dcp); +int iomfb_m4_suspend(struct apple_dcp *dcp); +int iomfb_m4_resume(struct apple_dcp *dcp); void iomfb_m4_configure_backlight(struct apple_dcp *dcp); void iomfb_m4_quiesce(struct apple_dcp *dcp); void iomfb_m4_debugfs_init(struct apple_dcp *dcp, struct dentry *root); @@ -50,6 +52,8 @@ static inline int iomfb_m4_present(struct apple_dcp *dcp, struct drm_framebuffer return -ENODEV; } static inline int iomfb_m4_blank(struct apple_dcp *dcp) { return -ENODEV; } +static inline int iomfb_m4_suspend(struct apple_dcp *dcp) { return -ENODEV; } +static inline int iomfb_m4_resume(struct apple_dcp *dcp) { return -ENODEV; } static inline void iomfb_m4_configure_backlight(struct apple_dcp *dcp) { } static inline void iomfb_m4_quiesce(struct apple_dcp *dcp) { } static inline void iomfb_m4_debugfs_init(struct apple_dcp *dcp, struct dentry *root) { } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py index 4a6c933743a63..b64693ff30a14 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py @@ -538,6 +538,7 @@ def blank(root): struct drm_framebuffer *active_fb,*uncertain_fb; struct dcp_m4_frame active_frame; u64 flips,blanks; + bool powered_off; }; static void mutex_lock(int *lock) { assert(!*lock); *lock=1; } static void mutex_unlock(int *lock) { assert(*lock==1); *lock=0; } @@ -597,6 +598,11 @@ def blank(root): assert(!iomfb_m4_blank(&owner)); assert(carried_brightness && carried_nits==0 && owner.backlight.state.actual==0); assert(fb.refs==1 && !dcp.active_fb && !dcp.active_frame.dva && dcp.blanks==1); + /* Powered down for system sleep: refused, no reference, session intact. */ + dcp.powered_off=true; + assert(iomfb_m4_present(&owner,&fb,0x10000100000ULL,0x2000000)==-EAGAIN); + assert(fb.refs==1 && !dcp.active_fb && !dcp.error); + dcp.powered_off=false; assert(!dcp_backlight_dpms(&owner,true)); assert(deferred_kicks==kicks_before+2); /* This second kick is still deferred. */ assert(!iomfb_m4_present(&owner,&fb,0x10000100000ULL,0x2000000)); @@ -611,6 +617,7 @@ def blank(root): } puts("PASS DPMS-off blank carries zero before delayed work and completes only after both exact receipts"); puts("PASS actual present/blank retain active+uncertain DMA framebuffer references across a missing receipt"); + puts("PASS a present while the display is powered off for sleep is refused without a reference"); return 0; } ''' From aec96e5158bcf357944ede683500ef837bd4c643 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Mon, 26 Oct 2026 17:20:53 -0400 Subject: [PATCH 4/5] drm/apple: m4: Power the display down while it is blanked With DPMS off, the J713 display was only blanked: the swap to no surface stops presenting, but the display pipeline stays powered. A blanked panel is the state in which system sleep powers the display down with setPowerState(0), and the same works here. After a successful blank, power the display down, and power it back up with setPowerState(1) before the next present, so the first frame after DPMS on brings the panel back. System sleep with the display already down leaves it down and skips its own setPowerState(0); after resume the next present powers it up. A refused transition fails the session, as in system sleep. debugfs reports whether the display is down, how often it went down, and how long the last and longest power-down and power-up took. On the J713, idle on battery with the backlight off, DPMS off saved 346 mW before this change (3.04 -> 2.69 W) and saves 1.20 W with it (2.93 -> 1.73 W), each measured within one run. Over 200 blank/wake cycles, power-down took 128-132 ms and power-up 55-56 ms. Signed-off-by: Erik Bourget [eryk: a blank that finds the display already blanked and powered down returns without a swap, which would otherwise reach the powered-down pipeline when a CRTC-only commit turned the CRTC on and off again] Signed-off-by: Eryk Wieliczko Co-Authored-By: Claude Opus 5.5 --- drivers/gpu/drm/apple/iomfb_m4.c | 49 ++++++++++++++++++- .../selftests/drivers/gpu/apple-dcp/m4.py | 19 +++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_m4.c b/drivers/gpu/drm/apple/iomfb_m4.c index f30610ee88c90..e10424923cfd5 100644 --- a/drivers/gpu/drm/apple/iomfb_m4.c +++ b/drivers/gpu/drm/apple/iomfb_m4.c @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -70,6 +72,9 @@ struct iomfb_m4 { int error; bool started, init_sent, ready, startup_started; bool powered_off; /* setPowerState(0) for system sleep */ + bool display_off; /* setPowerState(0) while blanked, until the next frame */ + u64 display_off_cycles; + s64 display_off_us, display_off_us_max, display_on_us, display_on_us_max; /* Exact completed successor or blank is the only framebuffer retirement. */ struct drm_framebuffer *active_fb; struct drm_framebuffer *uncertain_fb; @@ -78,6 +83,7 @@ struct iomfb_m4 { }; static void dcp_m4_runtime_wake(struct iomfb_m4 *dcp); +static int dcp_m4_set_power_state(struct iomfb_m4 *dcp, bool on); static void dcp_m4_runtime_receive(struct iomfb_m4 *dcp, u64 word); static int dcp_m4_dart_check(struct device *dev) @@ -1298,6 +1304,18 @@ int iomfb_m4_present(struct apple_dcp *owner, struct drm_framebuffer *fb, ret = -EAGAIN; if (!ret) ret = iomfb_m4_check_frame(&frame); + /* The first frame after a blank powers the display back up. */ + if (!ret && dcp->display_off) { + ktime_t start = ktime_get(); + + ret = dcp_m4_set_power_state(dcp, true); + if (!ret) { + dcp->display_off = false; + dcp->display_on_us = ktime_us_delta(ktime_get(), start); + dcp->display_on_us_max = max(dcp->display_on_us, + dcp->display_on_us_max); + } + } if (ret) goto out; /* Keep the source before the first work descriptor can become visible. */ @@ -1330,6 +1348,11 @@ int iomfb_m4_blank(struct apple_dcp *owner) return -ENODEV; mutex_lock(&dcp->lock); ret = dcp_m4_runtime_live(dcp->runtime); + /* Already blanked and powered down: nothing to swap or power down */ + if (!ret && dcp->display_off && !dcp->active_frame.dva) { + mutex_unlock(&dcp->lock); + return 0; + } if (!ret) ret = iomfb_m4_swap(dcp, NULL); if (ret) { @@ -1339,6 +1362,21 @@ int iomfb_m4_blank(struct apple_dcp *owner) dcp->active_fb = NULL; memset(&dcp->active_frame, 0, sizeof(dcp->active_frame)); dcp->blanks++; + /* + * Nothing is scanned out until the next frame: power the display + * down as system sleep does. A refusal fails the session, as there. + */ + if (!dcp->display_off) { + ktime_t start = ktime_get(); + + if (!dcp_m4_set_power_state(dcp, false)) { + dcp->display_off = true; + dcp->display_off_cycles++; + dcp->display_off_us = ktime_us_delta(ktime_get(), start); + dcp->display_off_us_max = max(dcp->display_off_us, + dcp->display_off_us_max); + } + } } mutex_unlock(&dcp->lock); if (retire) @@ -1394,11 +1432,14 @@ int iomfb_m4_suspend(struct apple_dcp *owner) if (dcp->active_fb || dcp->active_frame.dva) { dev_warn(dcp->dev, "panel still scanning out: refusing system sleep\n"); ret = -EBUSY; + } else if (dcp->display_off) { + /* Already powered down by the blank; the next frame powers it up. */ + ret = 0; } else { ret = dcp_m4_set_power_state(dcp, false); + if (!ret) + dcp->powered_off = true; } - if (!ret) - dcp->powered_off = true; mutex_unlock(&dcp->lock); return ret; } @@ -1500,6 +1541,10 @@ static int iomfb_m4_status_show(struct seq_file *s, void *unused) seq_printf(s, "flips=%llu blanks=%llu backlight_swaps=%llu active=%u uncertain=%u\n", dcp->flips, dcp->blanks, dcp->brightness_swaps, !!dcp->active_fb, !!dcp->uncertain_fb); + seq_printf(s, "display_off=%u off_cycles=%llu off_us=%lld/%lld on_us=%lld/%lld\n", + dcp->display_off, dcp->display_off_cycles, + dcp->display_off_us, dcp->display_off_us_max, + dcp->display_on_us, dcp->display_on_us_max); mutex_unlock(&dcp->lock); return 0; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py index b64693ff30a14..6e1e0523caa2a 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py @@ -516,6 +516,12 @@ def blank(root): #define spin_lock_irqsave(p,f) do { (void)(p); (f)=0; } while(0) #define spin_unlock_irqrestore(p,f) do { (void)(p); (void)(f); } while(0) typedef u64 dma_addr_t; +typedef int64_t s64; +typedef s64 ktime_t; +#define max(x,y) ((x)>(y)?(x):(y)) +static ktime_t clock_ns; +static ktime_t ktime_get(void) { return clock_ns += 1000; } +static s64 ktime_us_delta(ktime_t later,ktime_t earlier) { return (later-earlier)/1000; } struct iomfb_m4; struct apple_dcp { struct iomfb_m4 *v26_6; @@ -539,6 +545,9 @@ def blank(root): struct dcp_m4_frame active_frame; u64 flips,blanks; bool powered_off; + bool display_off; + u64 display_off_cycles; + s64 display_off_us,display_off_us_max,display_on_us,display_on_us_max; }; static void mutex_lock(int *lock) { assert(!*lock); *lock=1; } static void mutex_unlock(int *lock) { assert(*lock==1); *lock=0; } @@ -550,6 +559,9 @@ def blank(root): static void drm_framebuffer_get(struct drm_framebuffer *fb) { fb->refs++; } static void drm_framebuffer_put(struct drm_framebuffer *fb) { assert(fb->refs); fb->refs--; } static void dcp_m4_failed(struct iomfb_m4 *dcp,int error) { if(!dcp->error) dcp->error=error; } +static unsigned power_offs,power_ons; +static int dcp_m4_set_power_state(struct iomfb_m4 *dcp,bool on) +{ assert(dcp->lock==1); if(on) power_ons++; else power_offs++; return 0; } static int dcp_m4_runtime_live(struct dcp_m4_runtime *r) { return r->dcp->error; } static int iomfb_m4_check_frame(const struct dcp_m4_frame *f) { return dcp_m4_frame_check(f); } @@ -594,18 +606,24 @@ def blank(root): assert(fb.refs==2 && dcp.active_fb==&fb && !dcp.uncertain_fb); assert(!dcp_backlight_dpms(&owner,false)); assert(deferred_kicks==kicks_before+1 && owner.backlight.state.actual==120); + unsigned offs_before=power_offs,ons_before=power_ons; /* No queued backlight worker has run: blank itself must carry zero. */ assert(!iomfb_m4_blank(&owner)); assert(carried_brightness && carried_nits==0 && owner.backlight.state.actual==0); assert(fb.refs==1 && !dcp.active_fb && !dcp.active_frame.dva && dcp.blanks==1); + /* The blank powered the display down, once. */ + assert(dcp.display_off && dcp.display_off_cycles==1 && power_offs==offs_before+1); /* Powered down for system sleep: refused, no reference, session intact. */ dcp.powered_off=true; assert(iomfb_m4_present(&owner,&fb,0x10000100000ULL,0x2000000)==-EAGAIN); assert(fb.refs==1 && !dcp.active_fb && !dcp.error); + assert(dcp.display_off && power_ons==ons_before); dcp.powered_off=false; assert(!dcp_backlight_dpms(&owner,true)); assert(deferred_kicks==kicks_before+2); /* This second kick is still deferred. */ assert(!iomfb_m4_present(&owner,&fb,0x10000100000ULL,0x2000000)); + /* The first frame after the blank powered it back up. */ + assert(!dcp.display_off && power_ons==ons_before+1); assert(carried_brightness && carried_nits==120 && owner.backlight.state.actual==120); assert(fb.refs==2 && dcp.active_fb==&fb); failure=1; @@ -618,6 +636,7 @@ def blank(root): puts("PASS DPMS-off blank carries zero before delayed work and completes only after both exact receipts"); puts("PASS actual present/blank retain active+uncertain DMA framebuffer references across a missing receipt"); puts("PASS a present while the display is powered off for sleep is refused without a reference"); + puts("PASS a blank powers the display down once, and the next frame powers it back up"); return 0; } ''' From a150a02ca3534fb4debac9aa850179ae9c280240 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Mon, 26 Oct 2026 21:07:04 -0400 Subject: [PATCH 5/5] drm/apple: m4: Cache a static frame while nothing is flipping With a static image on the J713 panel, the display keeps scanning the frame out of memory 60 times a second. The DCP can instead cache it (IdleCachingMethod 2), but only after the host registers a cache surface: a swap with intent 8 whose sole surface is a separate surface object. Setting the method alone does nothing. Once no flip has followed for idle_cache_ms (default 1000, 0 disables), set the method and submit that swap with a dedicated, retained surface of the panel geometry, which the host never writes. The DCP keeps presenting the frame on screen, steps through its idle states (D117) and asks for a fence (D578) on entry and on each later refresh of the cache, granted for the surface this host registered; the next present, brightness change or blank leaves the cache (D579, then state 0). The surface stays registered across exits, so later entries resubmit it. After a rollback without a flip, entry is retried after ten quiet periods. The surface is allocated at startup; without it, or if the DCP refuses the method, the cache stays off. debugfs reports the idle state, open fences and the number of entries. On the J713, idle on battery with the backlight off, this saves 335 mW (3.27 -> 2.93 W) within one run. Brightness changes, DPMS off/on and suspend-to-idle leave and re-enter the cache. Signed-off-by: Erik Bourget --- drivers/gpu/drm/apple/dcp-m4-callback.h | 19 +- drivers/gpu/drm/apple/dcp-m4-frame.h | 16 ++ drivers/gpu/drm/apple/dcp-m4-profile.h | 2 + drivers/gpu/drm/apple/iomfb_m4.c | 174 +++++++++++++++++- .../selftests/drivers/gpu/apple-dcp/m4.py | 4 + 5 files changed, 206 insertions(+), 9 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-m4-callback.h b/drivers/gpu/drm/apple/dcp-m4-callback.h index 738f803835469..2cf3f73790ade 100644 --- a/drivers/gpu/drm/apple/dcp-m4-callback.h +++ b/drivers/gpu/drm/apple/dcp-m4-callback.h @@ -19,6 +19,8 @@ struct dcp_m4_callback_ops { int (*event)(void *cookie, const char *name, const void *dictionary, size_t size); int (*property)(void *cookie, unsigned int method, const void *input, void *output); int (*memory)(void *cookie, unsigned int method, const void *input, void *output); + /* Idle caching: D117 state, D578/D579 fence, D580 cache-surface release. */ + int (*idle)(void *cookie, unsigned int method, const void *input, void *output); }; struct dcp_m4_callback_shape { @@ -43,7 +45,8 @@ static const struct dcp_m4_callback_shape dcp_m4_callbacks[] = { {400, 76, 3076}, {401, 80, 12}, {406, 72, 0}, {408, 8, 8}, {413, 4168, 4}, {414, 80, 4}, {415, 76, 4}, {451, 20, 28}, {454, 4, 4}, {552, 4164, 4}, {561, 4164, 4}, {563, 76, 4}, {565, 72, 4}, {567, 128, 4}, - {572, 4, 4}, {575, 88, 76}, {576, 4, 0}, {582, 8, 4}, {588, 0, 0}, {590, 1840, 0}, + {572, 4, 4}, {575, 88, 76}, {576, 4, 0}, {578, 4, 4}, {579, 0, 0}, {580, 0, 0}, + {582, 8, 4}, {588, 0, 0}, {590, 1840, 0}, {592, 20, 0}, {594, 4, 0}, {599, 0, 0}, {700, 4, 4}, }; @@ -139,11 +142,19 @@ static inline int dcp_m4_callback_dispatch(const struct dcp_m4_callback_ops *ops case 300: case 406: case 576: case 588: return 0; /* - * will_power_off_signal, set_power_trace_enable, set_idle_caching_state_ap, - * swap_complete_intent, enable_backlight_message_ap_gated: notifications. + * will_power_off_signal, set_power_trace_enable, swap_complete_intent, + * enable_backlight_message_ap_gated: notifications. */ - case 2: case 8: case 117: case 592: case 594: + case 2: case 8: case 592: case 594: return 0; + /* + * set_idle_caching_state_ap, idle_fence_create, idle_fence_complete, + * idle_surface_release. + */ + case 117: case 578: case 579: case 580: + if (!ops->idle) + return -EOPNOTSUPP; + return dcp_m4_callback_result(ops->idle(cookie, method, input, output)); /* * acknowledgeSetPowerState, powerUpDART, set_kernel_power_assert: success; * aot_supported_peek: false. diff --git a/drivers/gpu/drm/apple/dcp-m4-frame.h b/drivers/gpu/drm/apple/dcp-m4-frame.h index 5ea5dc47f6443..cb84457f0794a 100644 --- a/drivers/gpu/drm/apple/dcp-m4-frame.h +++ b/drivers/gpu/drm/apple/dcp-m4-frame.h @@ -253,6 +253,22 @@ static inline int dcp_m4_swap_encode_full(void *output, size_t length, u32 swap_ return 0; } +/* Swap intent that registers surface 0 as the idle-cache surface. */ +#define DCP_M4_SWAP_INTENT_IDLE_CACHE 8 + +/* + * Retarget an encoded present: the DCP registers rather than presents its + * sole surface, which must be a distinct object (surface_id other than + * the primary's 1) from every surface on screen. + */ +static inline void dcp_m4_swap_set_intent(void *output, u32 intent, u32 surface_id) +{ + u8 *data = output; + + put_unaligned_le32(intent, data + dcp_m4_pf->swap_intent); + put_unaligned_le32(surface_id, data + 0x9c); +} + static inline int dcp_m4_swap_encode_checked(void *output, size_t length, u32 swap_id, const struct dcp_m4_frame *frame, bool brightness_change, u32 nits, diff --git a/drivers/gpu/drm/apple/dcp-m4-profile.h b/drivers/gpu/drm/apple/dcp-m4-profile.h index a7a91c1a4f397..4cbed603d3166 100644 --- a/drivers/gpu/drm/apple/dcp-m4-profile.h +++ b/drivers/gpu/drm/apple/dcp-m4-profile.h @@ -15,6 +15,7 @@ struct dcp_m4_profile { u32 swap_nullable; /* ten nullable surface bytes */ u32 swap_tail; /* final two bytes */ u32 swap_nits; /* binary64 brightness */ + u32 swap_intent; /* swap intent, 0 for a present */ u32 disabled_color; /* background with no surface */ u16 done_method; /* swap completion callback */ u32 done_size; @@ -28,6 +29,7 @@ static const struct dcp_m4_profile dcp_m4_profile_j713 = { .panel_width = 2560, .panel_height = 1664, .boot_stride = 10240, .swap_size = 0x1bd8, .swap_surface = 0x588, .swap_dva = 0xe38, .swap_nullable = 0x1bcb, .swap_tail = 0x1bd6, .swap_nits = 0x35e, + .swap_intent = 0x17c, .disabled_color = 0xff000000, .done_method = 590, .done_size = 0x730, .open_method = 457, .brightness_max = 500, .brightness_default = 120, diff --git a/drivers/gpu/drm/apple/iomfb_m4.c b/drivers/gpu/drm/apple/iomfb_m4.c index e10424923cfd5..d11e640b761b5 100644 --- a/drivers/gpu/drm/apple/iomfb_m4.c +++ b/drivers/gpu/drm/apple/iomfb_m4.c @@ -48,6 +48,22 @@ #include "dcp-m4-property.h" #include "dcp-m4-frame.h" +static unsigned int idle_cache_ms = 1000; + +static int idle_cache_ms_set(const char *val, const struct kernel_param *kp) +{ + return param_set_uint_minmax(val, kp, 0, 60000); +} + +static const struct kernel_param_ops idle_cache_ms_ops = { + .set = idle_cache_ms_set, + .get = param_get_uint, +}; + +module_param_cb(idle_cache_ms, &idle_cache_ms_ops, &idle_cache_ms, 0644); +MODULE_PARM_DESC(idle_cache_ms, + "J713: cache a static frame after this many ms without a flip (0: never, at most 60000; applies from the next swap)"); + struct iomfb_m4 { struct apple_dcp *owner; struct device *dev; @@ -80,6 +96,7 @@ struct iomfb_m4 { struct drm_framebuffer *uncertain_fb; struct dcp_m4_frame active_frame; u64 flips, blanks, brightness_swaps; + struct delayed_work idle_cache; }; static void dcp_m4_runtime_wake(struct iomfb_m4 *dcp); @@ -435,6 +452,16 @@ struct dcp_m4_runtime { bool qualified; /* KMS startup qualification passed; see dcp_m4_runtime_live() */ void *scanout_cpu; struct dcp_m4_frame scanout_frame; + /* + * Idle caching: one cache surface, allocated at startup, retained like + * all DCP-visible DMA and never written by the host. + */ + void *idle_cpu; + struct dcp_m4_frame idle_frame; + u32 idle_state, idle_fences; + bool idle_registered; /* from its first intent swap until D580 */ + bool idle_unavailable; + u64 idle_entries; }; static void dcp_m4_runtime_wake(struct iomfb_m4 *dcp) @@ -571,12 +598,54 @@ static int dcp_m4_runtime_swap_done(void *cookie, u32 id) return dcp_m4_swap_record_done(&r->swap, id); } +static int dcp_m4_runtime_idle_cache(void *cookie, unsigned int method, + const void *input, void *output) +{ + struct dcp_m4_runtime *r = cookie; + struct iomfb_m4 *dcp = r->dcp; + unsigned int ms = READ_ONCE(idle_cache_ms); + bool grant; + + switch (method) { + case 117: /* set_idle_caching_state_ap(state, size, flag) */ + r->idle_state = get_unaligned_le32(input); + if (r->idle_state) + return 0; + /* Leaving the cache completes every fence first. */ + r->idle_fences = 0; + /* Retry after a rollback without a flip, backing off. */ + if (ms && !READ_ONCE(dcp->owner->quiescing)) + queue_delayed_work(r->wq, &dcp->idle_cache, msecs_to_jiffies(10 * ms)); + return 0; + case 578: /* idle_fence_create(state) */ + /* + * The host never writes the cache surface, so a fence has no + * host-side writer to wait for. Entry (state 1) and each cache + * refresh (state 6) ask for one; grant them only for the surface + * this host registered. + */ + grant = r->idle_registered && !dcp->display_off && !dcp->powered_off; + r->idle_fences += grant; + put_unaligned_le32(grant, output); + return 0; + case 579: /* idle_fence_complete */ + if (r->idle_fences) + r->idle_fences--; + return 0; + case 580: /* idle_surface_release */ + r->idle_registered = false; + return 0; + } + return -EOPNOTSUPP; +} + static const struct dcp_m4_callback_ops dcp_m4_runtime_ops = { .call = dcp_m4_runtime_call, .property = dcp_m4_runtime_property, .memory = dcp_m4_runtime_memory, .event = dcp_m4_runtime_event, .swap_done = dcp_m4_runtime_swap_done, + .idle = dcp_m4_runtime_idle_cache, }; static void dcp_m4_runtime_bad_word(struct dcp_m4_runtime *r, u64 word, int error) @@ -765,11 +834,12 @@ static void dcp_m4_runtime_idle(struct work_struct *work) /* * Serialized owner only; both the A407 reply and its D590 fence must complete. - * primary/frame/upper/keep as dcp_m4_swap_encode_full(). + * primary/frame/upper/keep as dcp_m4_swap_encode_full(); a nonzero intent + * retargets the swap as dcp_m4_swap_set_intent() describes. */ static int dcp_m4_runtime_swap_full(struct dcp_m4_runtime *r, bool primary, const struct dcp_m4_frame *frame, const struct dcp_m4_upper *upper, u32 keep, - bool brightness_change, u32 nits) + bool brightness_change, u32 nits, u32 intent, u32 surface_id) { struct iomfb_m4 *dcp = r->dcp; u8 start[16] = {0}, started[8], submitted[12]; @@ -798,6 +868,8 @@ static int dcp_m4_runtime_swap_full(struct dcp_m4_runtime *r, bool primary, if (!ret) ret = dcp_m4_swap_encode_full(input, DCP_M4_SWAP_SIZE, id, primary, frame, upper, keep, brightness_change, nits, dcp_m4_frame_check); + if (!ret && intent) + dcp_m4_swap_set_intent(input, intent, surface_id); if (!ret) ret = dcp_m4_runtime_call(r, 407, false, input, DCP_M4_SWAP_SIZE, submitted, sizeof(submitted)); @@ -828,7 +900,7 @@ static int dcp_m4_runtime_swap_full(struct dcp_m4_runtime *r, bool primary, static int dcp_m4_runtime_swap_brightness(struct dcp_m4_runtime *r, const struct dcp_m4_frame *frame, bool brightness_change, u32 nits) { - return dcp_m4_runtime_swap_full(r, true, frame, NULL, 0, brightness_change, nits); + return dcp_m4_runtime_swap_full(r, true, frame, NULL, 0, brightness_change, nits, 0, 0); } static int dcp_m4_runtime_swap(struct dcp_m4_runtime *r, const struct dcp_m4_frame *frame) @@ -980,6 +1052,17 @@ static int dcp_m4_prepare_scanout(struct iomfb_m4 *dcp) /* Prepare backing while simpledrm still owns the inherited surface. */ memset(r->scanout_cpu, 0, r->scanout_frame.backing_size); dma_wmb(); + /* The idle cache is optional: without its (zeroed) surface it stays off. */ + r->idle_cpu = apple_dart_dcp_scanout_alloc(dcp->scanout, r->scanout_frame.backing_size, + &dva); + if (IS_ERR(r->idle_cpu)) { + dev_warn(dcp->dev, "no idle-cache surface: %ld\n", PTR_ERR(r->idle_cpu)); + r->idle_cpu = NULL; + r->idle_unavailable = true; + } else { + r->idle_frame = r->scanout_frame; + r->idle_frame.dva = dva; + } return dcp_m4_runtime_health(r); } @@ -1017,6 +1100,7 @@ static int iomfb_m4_check_frame(const struct dcp_m4_frame *frame) } static void iomfb_m4_backlight_work(struct work_struct *work); +static void iomfb_m4_idle_cache_work(struct work_struct *work); /* Once adopted, even driver unbind must preserve the callback cookie and DMA. */ static void iomfb_m4_release(void *cookie) @@ -1082,6 +1166,7 @@ int iomfb_m4_init(struct apple_dcp *owner) spin_lock_init(&dcp->state_lock); init_completion(&dcp->reply); INIT_WORK(&dcp->backlight_work, iomfb_m4_backlight_work); + INIT_DELAYED_WORK(&dcp->idle_cache, iomfb_m4_idle_cache_work); if (!request_mem_region(res->start, resource_size(res), dev_name(dev))) { ret = -EBUSY; goto release; @@ -1263,6 +1348,71 @@ int iomfb_m4_health(struct apple_dcp *owner) return ret; } +/* Any surface ID other than the primary's 1 names a distinct surface object. */ +#define DCP_M4_IDLE_SURFACE_ID 2 + +/* + * Register the idle cache behind the frame on screen: IdleCachingMethod 2, + * then a swap with intent 8 whose sole surface is the cache surface. The + * DCP keeps presenting the frame, steps through idle states 1, 2 and 4 + * (D117), asking for a fence (D578) in state 1 and whenever it refreshes the + * cache (4, 6, 2, 4), and leaves the cache at the next swap. + * The surface stays registered across exits, so later entries resubmit it. + */ +static int dcp_m4_idle_cache_enter(struct iomfb_m4 *dcp) +{ + struct dcp_m4_runtime *r = dcp->runtime; + struct { __le32 id, value; } method = { cpu_to_le32(126), cpu_to_le32(2) }; + __le32 out = 0; + int ret; + + lockdep_assert_held(&dcp->lock); + if (r->idle_unavailable || dcp->powered_off || dcp->display_off || !dcp->active_fb || + r->idle_state || r->idle_fences) + return 0; + ret = dcp_m4_runtime_live(r); + if (ret) + return ret; + ret = dcp_m4_runtime_call_stream(r, 467, 0, false, &method, sizeof(method), + &out, sizeof(out)); + if (ret) + return ret; + /* applyProperty status: a refused method stays refused. */ + if (out) { + dev_warn(dcp->dev, "idle caching refused: %#x\n", le32_to_cpu(out)); + r->idle_unavailable = true; + return 0; + } + /* D578 can arrive before the swap completes. */ + r->idle_registered = true; + /* Like any primary present, this drops upper surfaces; none are in use yet. */ + ret = dcp_m4_runtime_swap_full(r, true, &r->idle_frame, NULL, 0, false, 0, + DCP_M4_SWAP_INTENT_IDLE_CACHE, DCP_M4_IDLE_SURFACE_ID); + if (!ret) + r->idle_entries++; + return ret; +} + +/* Called with dcp->lock after a swap: enter the cache once no flip follows. */ +static void iomfb_m4_idle_cache_kick(struct iomfb_m4 *dcp) +{ + unsigned int ms = READ_ONCE(idle_cache_ms); + + if (ms && !READ_ONCE(dcp->owner->quiescing)) + mod_delayed_work(dcp->runtime->wq, &dcp->idle_cache, msecs_to_jiffies(ms)); +} + +static void iomfb_m4_idle_cache_work(struct work_struct *work) +{ + struct iomfb_m4 *dcp = container_of(to_delayed_work(work), struct iomfb_m4, idle_cache); + + mutex_lock(&dcp->lock); + /* An RPC failure has already failed the session. */ + if (!READ_ONCE(dcp->owner->quiescing) && dcp->runtime && READ_ONCE(idle_cache_ms)) + dcp_m4_idle_cache_enter(dcp); + mutex_unlock(&dcp->lock); +} + /* dcp->lock is the single owner of swaps, active descriptors and brightness. */ static int iomfb_m4_swap(struct iomfb_m4 *dcp, const struct dcp_m4_frame *frame) { @@ -1331,6 +1481,7 @@ int iomfb_m4_present(struct apple_dcp *owner, struct drm_framebuffer *fb, dcp->active_fb = fb; dcp->active_frame = frame; dcp->flips++; + iomfb_m4_idle_cache_kick(dcp); out: mutex_unlock(&dcp->lock); if (retire) @@ -1416,6 +1567,11 @@ static int dcp_m4_set_power_state(struct iomfb_m4 *dcp, bool on) ret = -EIO; dcp_m4_failed(dcp, ret); } + /* A display powered back up starts outside the idle cache. */ + if (!ret && on) { + dcp->runtime->idle_state = 0; + dcp->runtime->idle_fences = 0; + } return ret; } @@ -1477,10 +1633,12 @@ static void iomfb_m4_backlight_work(struct work_struct *work) ret = dcp_m4_runtime_live(dcp->runtime); if (!ret) ret = iomfb_m4_swap(dcp, &dcp->active_frame); - if (ret) + if (ret) { dcp_m4_failed(dcp, ret); - else + } else { dcp->brightness_swaps++; + iomfb_m4_idle_cache_kick(dcp); + } out: mutex_unlock(&dcp->lock); } @@ -1516,6 +1674,8 @@ void iomfb_m4_quiesce(struct apple_dcp *owner) cancel_work_sync(&dcp->backlight_work); if (dcp->runtime) flush_workqueue(dcp->runtime->wq); + /* After the flush, as drained callbacks can queue it. */ + cancel_delayed_work_sync(&dcp->idle_cache); } static int iomfb_m4_status_show(struct seq_file *s, void *unused) @@ -1545,6 +1705,10 @@ static int iomfb_m4_status_show(struct seq_file *s, void *unused) dcp->display_off, dcp->display_off_cycles, dcp->display_off_us, dcp->display_off_us_max, dcp->display_on_us, dcp->display_on_us_max); + if (r) + seq_printf(s, "idle_cache=%u state=%u fences=%u registered=%u entries=%llu\n", + !r->idle_unavailable && READ_ONCE(idle_cache_ms), r->idle_state, + r->idle_fences, r->idle_registered, r->idle_entries); mutex_unlock(&dcp->lock); return 0; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py index 6e1e0523caa2a..7fc32185e318d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py @@ -559,6 +559,8 @@ def blank(root): static void drm_framebuffer_get(struct drm_framebuffer *fb) { fb->refs++; } static void drm_framebuffer_put(struct drm_framebuffer *fb) { assert(fb->refs); fb->refs--; } static void dcp_m4_failed(struct iomfb_m4 *dcp,int error) { if(!dcp->error) dcp->error=error; } +static unsigned cache_kicks; +static void iomfb_m4_idle_cache_kick(struct iomfb_m4 *dcp) { assert(dcp->lock==1); cache_kicks++; } static unsigned power_offs,power_ons; static int dcp_m4_set_power_state(struct iomfb_m4 *dcp,bool on) { assert(dcp->lock==1); if(on) power_ons++; else power_offs++; return 0; } @@ -624,6 +626,8 @@ def blank(root): assert(!iomfb_m4_present(&owner,&fb,0x10000100000ULL,0x2000000)); /* The first frame after the blank powered it back up. */ assert(!dcp.display_off && power_ons==ons_before+1); + /* Every shown frame restarts the idle-cache countdown. */ + assert(cache_kicks==2*order+2); assert(carried_brightness && carried_nits==120 && owner.backlight.state.actual==120); assert(fb.refs==2 && dcp.active_fb==&fb); failure=1;