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 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 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. */ 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: 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/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 ac68f001626aa..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; @@ -650,10 +651,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 +672,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 +682,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 +704,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) { @@ -1285,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) @@ -1335,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/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/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; 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") || 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 */ 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() diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py index cb3bac542a88c..543803ea441cf 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"); } @@ -518,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; } @@ -577,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)); @@ -591,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; } '''