From a47b1be571d3c3c9ffd45715934cad9074d7e8bd Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 01/10] 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 cb3bac542a88c..802581b345463 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 4c6e0593a7e777265c4937390d5391c5c904be09 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 02/10] 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 294603158ad6df7829d69d972a8b9c57e416578e Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 03/10] 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 --- drivers/gpu/drm/apple/dcp.c | 16 +++- drivers/gpu/drm/apple/iomfb_m4.c | 80 +++++++++++++++++++ drivers/gpu/drm/apple/iomfb_m4.h | 4 + .../selftests/drivers/gpu/apple-dcp/m4.py | 7 ++ 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 8a2e2c5bcbf10..8d2c159db2970 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -5998,9 +5998,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; @@ -6080,7 +6088,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; diff --git a/drivers/gpu/drm/apple/iomfb_m4.c b/drivers/gpu/drm/apple/iomfb_m4.c index 0940bb01a858f..29714538e8270 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,83 @@ 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); + if (dcp->active_fb) { + 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 802581b345463..543803ea441cf 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py @@ -532,6 +532,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; } @@ -591,6 +592,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)); @@ -605,6 +611,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 242a5cd87a23e8b9f4fedd235410305c6e16b8a3 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 04/10] iommu/apple-dart: Keep the retained J713 DARTs through suspend-to-idle The J713's retained DMA owners refuse system sleep: the radio's DART with managed streams, and the display DARTs, which borrow the running DCP's live page-table root. Neither has a suspend boundary of its own, and their state cannot be restored from a copy. Suspend-to-idle does not need one. These DARTs have no power domain of their own in the tree, and the DCP keeps the display domains on across s2idle, so their state stays where it is. Let them through for suspend-to-idle only, saving their registers solely for resume to check that nothing moved. Every other sleep state is still refused. A DART whose retained state changed cannot be trusted with its live DMA, and a resume error does not stop the PM core from resuming consumers. Quarantine the managed streams and the borrowed display consumers through the same paths as a translation fault before failing resume, so that their later maps are refused. A host test exercises the extracted check, quarantine and map functions. Signed-off-by: Erik Bourget --- drivers/iommu/apple-dart.c | 68 ++++-- .../drivers/apple/dart-s2idle-retention.py | 218 ++++++++++++++++++ 2 files changed, 272 insertions(+), 14 deletions(-) create mode 100755 tools/testing/selftests/drivers/apple/dart-s2idle-retention.py diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index ab61aa391c55b..874351f215ef1 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -310,6 +311,8 @@ struct apple_dart { struct apple_dart_dcp *dcp; struct apple_dart_dcp *frontend[2]; bool preserve_display; + /* Retained radio/display state left in place across suspend-to-idle. */ + bool s2idle_kept; /* Resources reachable by a retained domain outlive device removal. */ bool retained; bool retained_mtp; @@ -4583,17 +4586,40 @@ static const struct apple_dart_hw apple_dart_hw_t8110 = { .ttbr_count = 1, }; +static void apple_dart_save_state(struct apple_dart *dart) +{ + unsigned int sid, idx; + + for (sid = 0; sid < dart->num_streams; sid++) { + dart->save_tcr[sid] = apple_dart_readl(dart, + DART_TCR(dart, sid)); + for (idx = 0; idx < dart->hw->ttbr_count; idx++) + dart->save_ttbr[sid][idx] = + apple_dart_readl(dart, + DART_TTBR(dart, sid, idx)); + } +} + static __maybe_unused int apple_dart_suspend(struct device *dev) { struct apple_dart *dart = dev_get_drvdata(dev); - unsigned int sid, idx; dart->tunnel_state_restored = false; if (READ_ONCE(dart->commands_gated)) return 0; - /* Neither retained radio nor display has a qualified suspend boundary. */ - if (dart->managed_sids || dart->preserve_display || apple_dart_has_borrowed(dart)) - return -EBUSY; + /* + * Retained radio and display DMA has no suspend boundary of its own. + * Suspend-to-idle leaves it running in place: these DARTs have no + * power domain here, and the DCP keeps the display domains on. The + * registers are saved only for resume to check. + */ + if (dart->managed_sids || dart->preserve_display || apple_dart_has_borrowed(dart)) { + if (pm_suspend_target_state != PM_SUSPEND_TO_IDLE) + return -EBUSY; + apple_dart_save_state(dart); + dart->s2idle_kept = true; + return 0; + } /* The tunneled DART has no state to save when its domain stays on. */ if (dart->power_retained || dart->retained_mtp) return 0; @@ -4602,15 +4628,7 @@ static __maybe_unused int apple_dart_suspend(struct device *dev) if (dart->locked) return 0; - for (sid = 0; sid < dart->num_streams; sid++) { - dart->save_tcr[sid] = apple_dart_readl(dart, - DART_TCR(dart, sid)); - for (idx = 0; idx < dart->hw->ttbr_count; idx++) - dart->save_ttbr[sid][idx] = - apple_dart_readl(dart, - DART_TTBR(dart, sid, idx)); - } - + apple_dart_save_state(dart); return 0; } @@ -4674,6 +4692,28 @@ static int apple_dart_invalidate_each_stream(struct apple_dart *dart) return 0; } +/* + * Resume of a DART whose live DMA stayed in place through suspend-to-idle. It + * cannot be restored from the saved copy. If the translations changed, the + * consumers' DMA is no longer what they set up: refuse their further use + * through the same paths as a translation fault before failing resume, since + * a resume error does not stop the PM core from resuming them. + */ +static int apple_dart_resume_retained(struct apple_dart *dart) +{ + if (!dart->s2idle_kept) + return -EBUSY; + dart->s2idle_kept = false; + if (apple_dart_state_retained(dart)) + return 0; + + dev_err(dart->dev, "retained DMA state lost in suspend-to-idle\n"); + if (dart->managed_sids) + apple_dart_managed_quarantine(dart); + apple_dart_quarantine_borrowed(dart); + return -EIO; +} + static __maybe_unused int apple_dart_resume(struct device *dev) { struct apple_dart *dart = dev_get_drvdata(dev); @@ -4681,7 +4721,7 @@ static __maybe_unused int apple_dart_resume(struct device *dev) int ret; if (dart->managed_sids || dart->preserve_display || apple_dart_has_borrowed(dart)) - return -EBUSY; + return apple_dart_resume_retained(dart); /* PCIe-C restored these mappings before starting its downstream link. */ if (dart->tunnel_state_restored) { diff --git a/tools/testing/selftests/drivers/apple/dart-s2idle-retention.py b/tools/testing/selftests/drivers/apple/dart-s2idle-retention.py new file mode 100755 index 0000000000000..44682a977f716 --- /dev/null +++ b/tools/testing/selftests/drivers/apple/dart-s2idle-retention.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise the suspend-to-idle resume check of retained DARTs on the host. + +The actual state check, quarantine and map functions are extracted from +apple-dart.c. A retained DART whose translation registers changed must fail +resume and quarantine its consumers, so that later maps are refused. These CPU +tests do not qualify physical retention on a J713. +""" +from pathlib import Path +import subprocess +import tempfile + + +def definition(source, name): + start = source.index(name) + start = source.rfind('\n', 0, start) + 1 + brace = source.index('{', start) + depth, end = 1, brace + 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +PRELUDE = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef u64 phys_addr_t; +typedef unsigned int gfp_t; +#define BIT(n) (1UL << (n)) +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define WRITE_ONCE(x, v) ((x) = (v)) +#define READ_ONCE(x) (x) +#define DART_MAX_STREAMS 32 +#define DART_MAX_TTBR 4 +#define DART_TCR(d, sid) (0x100 + 4 * (sid)) +#define DART_TTBR(d, sid, idx) (0x200 + 16 * (sid) + 4 * (idx)) +#define container_of(p, t, m) ((t *)((char *)(p) - offsetof(t, m))) +#define lockdep_assert_held(l) ((void)(l)) +#define spin_lock_irqsave(l, f) ((void)(l), (f) = 0) +#define spin_unlock_irqrestore(l, f) ((void)(l), (void)(f)) +static int errors; +#define dev_err(dev, ...) (errors++) +static void __set_bit(unsigned int n, unsigned long *p) { p[n / 64] |= 1UL << (n % 64); } + +struct apple_dart_hw { u32 tcr_bypass, ttbr_valid; unsigned int ttbr_count; }; +struct apple_dart; +struct apple_dart_dcp { bool failed; struct apple_dart *dart; }; +struct apple_dart { + const struct apple_dart_hw *hw; + u16 managed_sids; + bool managed_failed, supports_bypass, s2idle_kept; + int managed_lock; + struct { bool ready; } iommu; + struct apple_dart_dcp *dcp, *frontend[2]; + void *dev; + unsigned int num_streams; + u32 save_tcr[DART_MAX_STREAMS], save_ttbr[DART_MAX_STREAMS][DART_MAX_TTBR]; + u32 regs[0x400 / 4]; +}; +struct apple_dart_stream_map { struct apple_dart *dart; unsigned long sidmap[4]; }; +static u32 apple_dart_readl(struct apple_dart *d, unsigned int o) { return d->regs[o / 4]; } +static unsigned long disabled; +static void apple_dart_hw_disable_dma(struct apple_dart_stream_map *m) { disabled |= m->sidmap[0]; } + +struct iommu_domain { int unused; }; +struct io_pgtable_ops { + int (*map_pages)(struct io_pgtable_ops *, unsigned long, phys_addr_t, size_t, size_t, int, + gfp_t, size_t *); +}; +struct apple_dart_domain { + struct iommu_domain domain; + struct io_pgtable_ops *pgtbl_ops; + struct apple_dart_dcp *dcp; + struct apple_dart_stream_map stream_maps[1]; + u64 dma_offset, mask; +}; +#define to_dart_domain(d) container_of(d, struct apple_dart_domain, domain) +static int apple_dart_check_fw_map(struct apple_dart_domain *d, u64 dva, phys_addr_t pa, + size_t size, int prot) { return 0; } +static int apple_dart_dcp_map(struct apple_dart_dcp *dcp, unsigned long iova, phys_addr_t pa, + size_t pgsize, size_t count, int prot, gfp_t gfp, size_t *mapped) +{ assert(!"the managed radio domain is not a borrowed display domain"); return 0; } +static int maps; +static int test_map(struct io_pgtable_ops *ops, unsigned long iova, phys_addr_t pa, size_t pgsize, + size_t count, int prot, gfp_t gfp, size_t *mapped) +{ maps++; *mapped = pgsize * count; return 0; } +''' + +TEST = r''' +static const struct apple_dart_hw hw = { .tcr_bypass = 0, .ttbr_valid = 1, .ttbr_count = 1 }; +static struct apple_dart_dcp dcp, front0, front1; +static struct apple_dart d; +static struct io_pgtable_ops ops = { .map_pages = test_map }; +static struct apple_dart_domain radio; + +static void setup(void) +{ + memset(&d, 0, sizeof(d)); + dcp.failed = front0.failed = front1.failed = false; + d.hw = &hw; + d.num_streams = 16; + d.managed_sids = BIT(1) | BIT(2); + d.dcp = &dcp; + d.frontend[0] = &front0; + d.frontend[1] = &front1; + d.iommu.ready = true; + /* Live translations on the radio's streams and on an ordinary one. */ + d.regs[DART_TCR(&d, 1) / 4] = d.regs[DART_TCR(&d, 2) / 4] = d.regs[DART_TCR(&d, 5) / 4] = 1; + d.regs[DART_TTBR(&d, 1, 0) / 4] = 0x1001; + d.regs[DART_TTBR(&d, 2, 0) / 4] = 0x2001; + d.regs[DART_TTBR(&d, 5, 0) / 4] = 0x5001; + for (unsigned int sid = 0; sid < d.num_streams; sid++) { + d.save_tcr[sid] = d.regs[DART_TCR(&d, sid) / 4]; + d.save_ttbr[sid][0] = d.regs[DART_TTBR(&d, sid, 0) / 4]; + } + d.s2idle_kept = true; + disabled = 0; + maps = 0; + errors = 0; + radio = (struct apple_dart_domain){ .pgtbl_ops = &ops, .mask = ~0ULL, + .stream_maps = {{ .dart = &d }} }; +} + +static int map_16k(void) +{ + size_t mapped = 0; + return apple_dart_map_pages(&radio.domain, 0x10000, 0x800000000ULL, 0x4000, 1, 3, 0, &mapped); +} + +static void expect_quarantined(int ret) +{ + assert(ret == -EIO && errors == 1); + assert(d.managed_failed && disabled == d.managed_sids); + assert(dcp.failed && front0.failed && front1.failed); + assert(map_16k() == -EIO && maps == 0); +} + +int main(void) +{ + /* Retained: resume succeeds and the radio maps as before. */ + setup(); + assert(apple_dart_resume_retained(&d) == 0 && !d.s2idle_kept); + assert(!d.managed_failed && !disabled && !dcp.failed && !errors); + assert(map_16k() == 0 && maps == 1); + + /* A changed TTBR on a managed stream. */ + setup(); + d.regs[DART_TTBR(&d, 1, 0) / 4] = 0x9001; + expect_quarantined(apple_dart_resume_retained(&d)); + + /* A changed TCR on an ordinary stream of the same DART. */ + setup(); + d.regs[DART_TCR(&d, 5) / 4] = 0; + expect_quarantined(apple_dart_resume_retained(&d)); + + /* A DART that lost power reads back as reset. */ + setup(); + memset(d.regs, 0, sizeof(d.regs)); + memset(d.save_tcr, 0, sizeof(d.save_tcr)); + memset(d.save_ttbr, 0, sizeof(d.save_ttbr)); + expect_quarantined(apple_dart_resume_retained(&d)); + + /* No suspend-to-idle suspend: refused, nothing quarantined. */ + setup(); + d.s2idle_kept = false; + assert(apple_dart_resume_retained(&d) == -EBUSY); + assert(!d.managed_failed && !dcp.failed && map_16k() == 0); + + puts("PASS retained DART resume: kept state resumes; lost state quarantines and refuses maps"); + return 0; +} +''' + + +def main(): + root = Path(__file__).resolve().parents[5] + dart = (root / 'drivers/iommu/apple-dart.c').read_text() + code = PRELUDE + for name in ('static bool apple_dart_state_retained(', + 'static void apple_dart_managed_quarantine_locked(', + 'static void apple_dart_managed_quarantine(', + 'static void apple_dart_quarantine_borrowed(', + 'static int apple_dart_resume_retained(', + 'static int apple_dart_map_pages('): + code += definition(dart, name) + '\n' + code += TEST + quarantine = ('\tif (dart->managed_sids)\n\t\tapple_dart_managed_quarantine(dart);\n' + '\tapple_dart_quarantine_borrowed(dart);\n') + assert code.count(quarantine) == 1, 'the resume quarantine was not found' + with tempfile.TemporaryDirectory() as directory: + c, exe = Path(directory) / 'resume.c', Path(directory) / 'resume' + for control in ('production', 'missing-quarantine'): + tested = code if control == 'production' else code.replace(quarantine, '') + c.write_text(tested) + subprocess.run(['cc', '-std=c11', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-parameter', '-Wno-unused-function', + str(c), '-o', str(exe)], check=True) + result = subprocess.run([str(exe)], capture_output=True, text=True) + if control == 'production': + assert result.returncode == 0, result.stderr + print(result.stdout.strip()) + else: + assert result.returncode and 'Assertion' in result.stderr, result + print('Retained DART resume ' + control + ' negative control: detected at runtime') + + +if __name__ == '__main__': + main() From 35667cd8d6c7d594346476cb45dfcbb2c9d26af1 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 05/10] PCI: apple: Let the managed J713 radio through suspend-to-idle The J713's radio port, whose streams the OS adopts from the boot loader's handoff, refuses system sleep in its noirq phase. It has no suspend boundary of its own, but suspend-to-idle does not need one: the port has no power domain in the tree, so it stays up as any other internal port does, whose resume does nothing. Let it through for suspend-to-idle only; every other sleep state is still refused. Signed-off-by: Erik Bourget --- drivers/pci/controller/pcie-apple.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/pci/controller/pcie-apple.c b/drivers/pci/controller/pcie-apple.c index e0b6ead2526e2..9da32a393a9ce 100644 --- a/drivers/pci/controller/pcie-apple.c +++ b/drivers/pci/controller/pcie-apple.c @@ -3588,8 +3588,12 @@ static int apple_pcie_suspend_noirq(struct device *dev) { struct apple_pcie *pcie = apple_pcie_lookup(dev); + /* + * The managed radio has no suspend boundary of its own; suspend-to-idle + * leaves it in place like any other internal port (no power domain). + */ if (pcie->managed_sids) - return -EBUSY; + return pm_suspend_target_state == PM_SUSPEND_TO_IDLE ? 0 : -EBUSY; if (!pcie->hw->tunneled) return 0; pcie->reset_on_resume = false; From dc0c0cbfe9217af75b18b664d77e81798d2a0e14 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 06/10] dt-bindings: power: apple,pmgr-pwrstate: Add apple,sleep-retained A coprocessor can keep running in its power domain across system sleep without system-sleep callbacks of its own, as the J713's GPU firmware does. Describe such a domain, which the OS keeps on during system sleep; runtime power management is unaffected. Signed-off-by: Erik Bourget --- .../devicetree/bindings/power/apple,pmgr-pwrstate.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml b/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml index 919e53113fd2a..6ba123fbeba95 100644 --- a/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml +++ b/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml @@ -22,6 +22,7 @@ allOf: then: properties: apple,firmware-owned: false + apple,sleep-retained: false apple,always-on: false apple,min-state: false apple,force-disable: false @@ -106,6 +107,13 @@ properties: coprocessors' CPU domains. type: boolean + apple,sleep-retained: + description: + A coprocessor keeps running in this domain across system sleep with no + system-sleep callbacks of its own. The OS keeps the domain on during + system sleep. Runtime power management is unaffected. + type: boolean + apple,firmware-owned: description: The boot loader hands over active hardware whose power domain remains From cd474f82e03056f8ec099a79c3a9fda5c4ea233d Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 07/10] pmdomain: apple: pmgr-pwrstate: Keep sleep-retained domains on in system sleep The J713's G16 GPU has no system-sleep callbacks, so in the noirq phase genpd powers its GFX domain off, and the TVM gate above it, under the running GPU firmware; the power-on at resume then times out. Let a domain marked apple,sleep-retained decline its power-off while the system sleeps: genpd then keeps it and its parents on. Runtime power management is unchanged. Signed-off-by: Erik Bourget --- drivers/pmdomain/apple/pmgr-pwrstate.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c index bfcfb5ccbf1a4..103abc2fd8c40 100644 --- a/drivers/pmdomain/apple/pmgr-pwrstate.c +++ b/drivers/pmdomain/apple/pmgr-pwrstate.c @@ -51,6 +51,8 @@ struct apple_pmgr_ps { bool externally_clocked; bool system_sleep; bool firmware_owned; + /* apple,sleep-retained: kept on while the system sleeps */ + bool sleep_retained; /* Never enable automatic power management (apple,no-auto-pm). */ bool no_auto_pm; bool inherited_on; @@ -230,6 +232,9 @@ static int apple_pmgr_ps_power_off(struct generic_pm_domain *genpd) return 0; if (ps->inherited_on) return -EBUSY; + /* A live coprocessor keeps this domain through system sleep. */ + if (ps->sleep_retained && ps->system_sleep) + return -EBUSY; ret = apple_pmgr_ps_set(genpd, APPLE_PMGR_PS_PWRGATE, false); if (ret) @@ -437,6 +442,7 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) * vote and never read, reset or change the hardware state. */ ps->firmware_owned = of_property_read_bool(node, "apple,firmware-owned"); + ps->sleep_retained = of_property_read_bool(node, "apple,sleep-retained"); /* * Only T8132 hands over inherited-on domains with this contract. Older * M3 device trees carry the property with another meaning; keep them @@ -444,7 +450,8 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) */ ps->inherited_on = t8132 && of_property_read_bool(node, "apple,inherited-on"); if (ps->inherited_on && - (ps->firmware_owned || of_property_present(node, "apple,min-state") || + (ps->firmware_owned || ps->sleep_retained || + of_property_present(node, "apple,min-state") || of_property_present(node, "apple,always-on") || of_property_present(node, "apple,force-disable") || of_property_present(node, "apple,force-reset") || From 4521100fc17c2587a2236b91ab86e7fa019bff92 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 08/10] dt-bindings: mailbox: apple,mailbox: Add apple,hold-rx-in-sleep Some coprocessors message the AP periodically while idle: the J713's GPU firmware asks for ring service about twice a second, and its AOP sends a report every second. Describe a mailbox whose messages need not wake the system, so the OS can leave them in the FIFO while it sleeps and handle them at resume. Signed-off-by: Erik Bourget --- .../devicetree/bindings/mailbox/apple,mailbox.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml index a1dfa81a65014..071ba0f3a6fed 100644 --- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml @@ -114,6 +114,13 @@ properties: resets: maxItems: 1 + apple,hold-rx-in-sleep: + description: + Messages from this coprocessor need not wake the system. The OS may + leave them in the receive FIFO while the system sleeps and handle + them at resume. + type: boolean + power-domains: minItems: 1 maxItems: 2 From 35ee6995b191d655524c821f5d691958d6192b7c Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 09/10] soc: apple: mailbox: Hold the receive side across system sleep on request The receive IRQ of the ASC mailboxes is IRQF_NO_SUSPEND, so every coprocessor message wakes a CPU out of suspend-to-idle. On the J713 the GPU firmware's idle ring-service kicks and an AOP report every second wake it about three times a second: in 938 s of s2idle, 1472 wakeups and only 512 s in deep idle. For a mailbox marked apple,hold-rx-in-sleep, mask the receive IRQ from noirq suspend to noirq resume, so its messages wait in the FIFO and the consumer handles them on resume. The mailbox is a supplier of its consumer, so this runs after the consumer suspended and before it resumes. The firmware throttles once its outbox fills: after 148 s of s2idle, 11 GPU and 3 AOP messages were waiting, with 3 wakeups and 146.9 s in deep idle. Signed-off-by: Erik Bourget --- drivers/soc/apple/mailbox.c | 30 ++++++++++++++++++++++++++++++ include/linux/soc/apple/mailbox.h | 2 ++ 2 files changed, 32 insertions(+) diff --git a/drivers/soc/apple/mailbox.c b/drivers/soc/apple/mailbox.c index a85e5b585f319..97eba3078bab5 100644 --- a/drivers/soc/apple/mailbox.c +++ b/drivers/soc/apple/mailbox.c @@ -698,6 +698,7 @@ static int apple_mbox_probe(struct platform_device *pdev) return -ENOMEM; mbox->dev = &pdev->dev; + mbox->hold_rx_in_sleep = of_property_read_bool(dev->of_node, "apple,hold-rx-in-sleep"); mbox->hw = of_device_get_match_data(dev); if (!mbox->hw) return -EINVAL; @@ -992,10 +993,39 @@ static const struct of_device_id apple_mbox_of_match[] = { }; MODULE_DEVICE_TABLE(of, apple_mbox_of_match); +/* + * The receive IRQ is IRQF_NO_SUSPEND, so each message wakes a CPU out of + * suspend-to-idle. A coprocessor whose messages need not wake the system + * keeps them in the FIFO until resume. The mailbox is a supplier of its + * consumer, so this runs after the consumer suspended and before it resumes. + */ +static int apple_mbox_suspend_noirq(struct device *dev) +{ + struct apple_mbox *mbox = dev_get_drvdata(dev); + + if (mbox && mbox->hold_rx_in_sleep) + apple_mbox_hold_rx(mbox); + return 0; +} + +static int apple_mbox_resume_noirq(struct device *dev) +{ + struct apple_mbox *mbox = dev_get_drvdata(dev); + + if (mbox && mbox->hold_rx_in_sleep) + apple_mbox_release_rx(mbox); + return 0; +} + +static const struct dev_pm_ops apple_mbox_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(apple_mbox_suspend_noirq, apple_mbox_resume_noirq) +}; + static struct platform_driver apple_mbox_driver = { .driver = { .name = "apple-mailbox", .of_match_table = apple_mbox_of_match, + .pm = pm_sleep_ptr(&apple_mbox_pm_ops), }, .probe = apple_mbox_probe, .remove = apple_mbox_remove, diff --git a/include/linux/soc/apple/mailbox.h b/include/linux/soc/apple/mailbox.h index fcb17eaf2d5a3..6cbf100d03844 100644 --- a/include/linux/soc/apple/mailbox.h +++ b/include/linux/soc/apple/mailbox.h @@ -64,6 +64,8 @@ struct apple_mbox { bool active; bool ascwrap_v6_cpu_running; bool removing; + /* apple,hold-rx-in-sleep: receive IRQ masked across system sleep */ + bool hold_rx_in_sleep; int irq_recv_not_empty; int irq_send_empty; /* < 0: no send-empty IRQ, TX polls */ From 4fe8d2f1acfa8b718f7e7139f2ba2fa45eaf31ce Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 10/10] arm64: dts: apple: t8132-j713: Keep the GPU on and quiet in system sleep The G16 firmware keeps running across suspend-to-idle: keep its GFX domain on through system sleep, and hold its idle ring-service kicks in the mailbox instead of waking the system about twice a second. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi b/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi index 7cddbaf1e5b56..bd6bf689e84e7 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi @@ -80,6 +80,8 @@ /* ASCWrap v6 contains the same mailbox aperture used by ASC v4. */ compatible = "apple,t8132-asc-mailbox", "apple,asc-mailbox-v4"; reg = <0x3 0x02608000 0x0 0x4000>; + /* Idle ring-service kicks need not wake the system. */ + apple,hold-rx-in-sleep; interrupt-parent = <&aic>; interrupts = , , @@ -95,6 +97,8 @@ &ps_gfx { status = "okay"; power-domains = <&gfx_tvm>; + /* The G16 firmware stays live across suspend-to-idle. */ + apple,sleep-retained; }; /* The existing report provider owns readiness for shared PMP commands. */