On 2025-12-01 18:18, [email protected] wrote:
> From: Leo Li <[email protected]>
>
> [Why]
>
> APU DCN generations since DCN3.5 have the capability to power down
> almost all of the DCN hw block during idle periods. This is referred to
> as IPS -- idle power states. In combination with a panel remote-buffer
> feature (like PSR or Panel Replay), IPS can save additional power.
>
> Once DCN is in an IPS, no register access can occur. This includes
> control registers for vblank interrupts; IPS must first be exited.
>
> Transitioning in or out of IPS requires synchronization with the rest of
> DC, as it powers up or down DCN, and may communicate with other MCUs on
> the SOC to do so. This is done via the dc_lock mutex.
>
> While calling enable_vblank, the DRM vblank core holds spinlocks that
> prevent blocking operations. Yet acquiring the dc_lock mutex is
> blocking. Thus, IPS can not be exited piror to programming vblank
> interrupt registers from within enable_vblank. At least not in a
> race-free way.
>
> Prior to this change, amdgpu_dm was exiting IPS(*) without holding the
> dc_lock, opening the door for races:
> https://gitlab.freedesktop.org/drm/amd/-/issues/5233
>
> (*) From touching the interrupt registers. All register reads today have
> an implicit IPS exit, see dm_read_reg_func()
>
> To solve this, the prepare_vblank_enable callback can be implemented to
> exit IPS, as it is called from process context.
>
> [How]
>
> Implement the prepare_vblank_enable callback for amdgpu_dm. In it,
> the dc_lock mutex is acquired, and IPS is exited.
>
> v2: Add missing semicolon, add docstring for prepare_vbl_disallow_idle
>
> Signed-off-by: Leo Li <[email protected]>
Reviewed-by: Harry Wentland <[email protected]>
Harry
> ---
> .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c | 3 ++
> .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h | 9 +++++
> .../drm/amd/display/amdgpu_dm/amdgpu_dm_crc.c | 4 +++
> .../amd/display/amdgpu_dm/amdgpu_dm_crtc.c | 36 +++++++++++++++++--
> 4 files changed, 49 insertions(+), 3 deletions(-)
>
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
> b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
> index 0346052f2e574..842a93e2d6ce0 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
> @@ -9682,6 +9682,7 @@ static void amdgpu_dm_handle_vrr_transition(struct
> dm_crtc_state *old_state,
> * We also need vupdate irq for the actual core vblank handling
> * at end of vblank.
> */
> + WARN_ON(drm_crtc_vblank_prepare(new_state->base.crtc) != 0);
> WARN_ON(amdgpu_dm_crtc_set_vupdate_irq(new_state->base.crtc,
> true) != 0);
> WARN_ON(drm_crtc_vblank_get(new_state->base.crtc) != 0);
> drm_dbg_driver(new_state->base.crtc->dev, "%s: crtc=%u VRR
> off->on: Get vblank ref\n",
> @@ -10108,6 +10109,7 @@ static void amdgpu_dm_commit_planes(struct
> drm_atomic_state *state,
> */
> if (acrtc_attach->base.state->event &&
> acrtc_state->active_planes > 0) {
> + drm_crtc_vblank_prepare(pcrtc);
> drm_crtc_vblank_get(pcrtc);
>
> spin_lock_irqsave(&pcrtc->dev->event_lock, flags);
> @@ -10124,6 +10126,7 @@ static void amdgpu_dm_commit_planes(struct
> drm_atomic_state *state,
> &acrtc_state->stream->vrr_infopacket;
> }
> } else if (cursor_update && acrtc_state->active_planes > 0) {
> + drm_crtc_vblank_prepare(pcrtc);
> spin_lock_irqsave(&pcrtc->dev->event_lock, flags);
> if (acrtc_attach->base.state->event) {
> drm_crtc_vblank_get(pcrtc);
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
> b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
> index 7065b20aa2e6b..a99612fb3467a 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
> @@ -587,6 +587,15 @@ struct amdgpu_display_manager {
> */
> uint32_t active_vblank_irq_count;
>
> + /**
> + * @prepare_vbl_disallow_idle:
> + *
> + * Set to true when idle has been disallowed. Set to false when vblank
> + * interrupts have been enabled. i.e. idle re-allow on vblank disable is
> + * blocked if this is true.
> + */
> + bool prepare_vbl_disallow_idle;
> +
> #if defined(CONFIG_DRM_AMD_SECURE_DISPLAY)
> /**
> * @secure_display_ctx:
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_crc.c
> b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_crc.c
> index e20aa74380665..7839b56859391 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_crc.c
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_crc.c
> @@ -656,6 +656,10 @@ int amdgpu_dm_crtc_set_crc_source(struct drm_crtc *crtc,
> const char *src_name)
> */
> enabled = amdgpu_dm_is_valid_crc_source(cur_crc_src);
> if (!enabled && enable) {
> + ret = drm_crtc_vblank_prepare(crtc);
> + if (ret)
> + goto cleanup;
> +
> ret = drm_crtc_vblank_get(crtc);
> if (ret)
> goto cleanup;
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_crtc.c
> b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_crtc.c
> index 38f9ea313dcbb..dd693419111db 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_crtc.c
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_crtc.c
> @@ -258,8 +258,8 @@ static void amdgpu_dm_crtc_vblank_control_worker(struct
> work_struct *work)
> else if (dm->active_vblank_irq_count)
> dm->active_vblank_irq_count--;
>
> - if (dm->active_vblank_irq_count > 0)
> - dc_allow_idle_optimizations(dm->dc, false);
> + /* prepare_vblank_enable must disallow idle first */
> + ASSERT(dm->dc->idle_optimizations_allowed == false);
>
> /*
> * Control PSR based on vblank requirements from OS
> @@ -277,7 +277,13 @@ static void amdgpu_dm_crtc_vblank_control_worker(struct
> work_struct *work)
> vblank_work->acrtc->dm_irq_params.allow_sr_entry);
> }
>
> - if (dm->active_vblank_irq_count == 0) {
> + /*
> + * If this worker runs disable between prepare_vblank and enable_vblank,
> + * we need to block idle re-allow. Leave it to the next vblank disable
> + * to re-allow idle.
> + */
> + if (dm->active_vblank_irq_count == 0 &&
> + !READ_ONCE(dm->prepare_vbl_disallow_idle)) {
> dc_post_update_surfaces_to_stream(dm->dc);
>
> r = amdgpu_dpm_pause_power_profile(adev, true);
> @@ -308,6 +314,8 @@ static inline int amdgpu_dm_crtc_set_vblank(struct
> drm_crtc *crtc, bool enable)
> int irq_type;
> int rc = 0;
>
> + ASSERT(dm->dc->idle_optimizations_allowed == false);
> +
> if (enable && !acrtc->base.enabled) {
> drm_dbg_vbl(crtc->dev,
> "Reject vblank enable on unconfigured CRTC %d
> (enabled=%d)\n",
> @@ -399,6 +407,9 @@ static inline int amdgpu_dm_crtc_set_vblank(struct
> drm_crtc *crtc, bool enable)
> }
> #endif
>
> + /* Ensure compiler emits the write before worker is queued */
> + WRITE_ONCE(dm->prepare_vbl_disallow_idle, false);
> +
> if (amdgpu_in_reset(adev))
> return 0;
>
> @@ -423,6 +434,24 @@ static inline int amdgpu_dm_crtc_set_vblank(struct
> drm_crtc *crtc, bool enable)
> return 0;
> }
>
> +static int amdgpu_prepare_enable_vblank(struct drm_crtc *crtc)
> +{
> + struct amdgpu_device *adev = drm_to_adev(crtc->dev);
> + struct amdgpu_display_manager *dm = &adev->dm;
> +
> + guard(mutex)(&adev->dm.dc_lock);
> +
> + if (dm->dc->idle_optimizations_allowed) {
> + /* Prevent the disable worker from re-allowing idle until
> + * interrupts are enabled. Ensure compiler emits the write
> + * before disallowing idle. */
> + WRITE_ONCE(dm->prepare_vbl_disallow_idle, true);
> + dc_allow_idle_optimizations(dm->dc, false);
> + }
> +
> + return 0;
> +}
> +
> int amdgpu_dm_crtc_enable_vblank(struct drm_crtc *crtc)
> {
> return amdgpu_dm_crtc_set_vblank(crtc, true);
> @@ -590,6 +619,7 @@ static const struct drm_crtc_funcs amdgpu_dm_crtc_funcs =
> {
> .verify_crc_source = amdgpu_dm_crtc_verify_crc_source,
> .get_crc_sources = amdgpu_dm_crtc_get_crc_sources,
> .get_vblank_counter = amdgpu_get_vblank_counter_kms,
> + .prepare_enable_vblank = amdgpu_prepare_enable_vblank,
> .enable_vblank = amdgpu_dm_crtc_enable_vblank,
> .disable_vblank = amdgpu_dm_crtc_disable_vblank,
> .get_vblank_timestamp = drm_crtc_vblank_helper_get_vblank_timestamp,