Re: [PATCH v2] drm/msm/dpu: stop the slave video interfaces before disabling a split encoder
From: Dmitry Baryshkov
Date: Wed Sep 30 2026 - 22:50:31 EST
On Wed, Sep 30, 2026 at 02:48:19AM +0900, Joonhoe Kim wrote:
> dpu_encoder_virt_atomic_disable() disables the physical encoders one by
> one. For a video-mode master, dpu_encoder_phys_vid_disable() stops its
> timing engine, waits for the frame to finish and then runs
> dpu_encoder_helper_phys_cleanup(), which resets the CTL. With a split
> display (two interfaces driven from one CTL, e.g. bonded DSI) that CTL
> is shared with the slave, whose timing engine is still running at that
> point: the source pipe starts fetching the slave's next frame and is
> left stalled half-way through it (on SM8850, SSPP_CMN_STATUS 0x10030
> with the fetch and unpack counters frozen, where an idle pipe shows
> 0x10003).
>
> The stall is cleared by a power collapse of the MDSS core GDSC, which
> normally happens between a disable and the next enable, so it goes
> unnoticed. When MDSS stays powered across the disable -- a full modeset
> within one commit, or another runtime-active user of MDSS such as the
> DP controller -- the next enable of the bonded DSI panel scans out
> nothing: the DPU keeps committing frames, the layer mixers produce no
> output (CRC 0), and the panel shows black with the backlight on. A CTL
> reset at enable does not clear it.
>
> Stop the timing engines of the slave interfaces before the master is
> disabled. The master keeps stopping its own timing engine in
> dpu_encoder_phys_vid_disable(), which counts the final vsync before it
> waits for it, and its cleanup then finds the slaves idle.
>
> Seen on a Lenovo Legion Tab Y700 gen 5 (TB323FU, SM8850) with a bonded
> DSI video-mode panel (CSOT PP8807HB1-1). It reproduces without any
> external display by keeping MDSS runtime-active:
>
> echo on > /sys/bus/platform/devices/9800000.display-subsystem/power/control
>
> then DPMS off and on from the compositor: black 3/3 before this change.
> Every full modeset (e.g. a refresh rate change) went black the same way.
> With this change: DPMS off/on 15/15 show the picture, with no "wait
> disable failed" timeouts. Only tested on this device.
>
> Fixes: 22cb02bc96ff ("drm/msm/disp/dpu: reset the datapath after timing engine disable")
> Assisted-by: LLM
> Signed-off-by: Joonhoe Kim <26rote@xxxxxxxxx>
> ---
> Changes in v2:
> - Stop only the slave interfaces up front and leave the master to
> dpu_encoder_phys_vid_disable(), which counts the final vsync before it
> stops the timing engine and waits for it; v1 stopped the master too,
> so that vsync could arrive before it was counted and the wait could
> time out (Sashiko review). Subject updated to match.
> - Link to v1: https://lore.kernel.org/all/20260929130621.943-1-26rote@xxxxxxxxx/
>
> Saim Shujah's "drm/msm/dpu: clear pending flush state before physical cleanup"
> (https://lore.kernel.org/all/20260826182459.1506522-1-saimzst@xxxxxxxxx/)
> alone does not help here: still black 3/3 with only that change.
>
> drivers/gpu/drm/msm/disp/dpu1/dpu_encoder.c | 33 +++++++++++++++++++++
> 1 file changed, 33 insertions(+)
>
> diff --git a/drivers/gpu/drm/msm/disp/dpu1/dpu_encoder.c b/drivers/gpu/drm/msm/disp/dpu1/dpu_encoder.c
> index 1f20695f81e3..2f8b6204c93e 100644
> --- a/drivers/gpu/drm/msm/disp/dpu1/dpu_encoder.c
> +++ b/drivers/gpu/drm/msm/disp/dpu1/dpu_encoder.c
> @@ -1380,6 +1380,36 @@ static void dpu_encoder_virt_atomic_enable(struct drm_encoder *drm_enc,
> mutex_unlock(&dpu_enc->enc_lock);
> }
>
> +/*
> + * Stop the timing engines of the slave video interfaces of a split display
> + * (two interfaces on one CTL, e.g. bonded DSI) before the master is disabled.
> + * The master's cleanup resets the shared CTL; with a slave timing engine still
> + * running, the source pipes start fetching the slave's next frame and stall
> + * half-way through it. The stall survives until the MDSS core GDSC is
> + * power-collapsed, so when MDSS stays powered the next enable scans out
> + * nothing. The master itself is stopped by its own disable path, which counts
> + * the final vsync it waits for.
> + */
Drop.
> +static void dpu_encoder_stop_slave_timing(struct dpu_encoder_virt *dpu_enc)
> +{
> + unsigned long lock_flags;
> + int i;
> +
> + for (i = 0; i < dpu_enc->num_phys_encs; i++) {
> + struct dpu_encoder_phys *phys = dpu_enc->phys_encs[i];
> +
> + if (phys->split_role != ENC_ROLE_SLAVE ||
> + phys->intf_mode != INTF_MODE_VIDEO || !phys->hw_intf ||
> + !phys->hw_intf->ops.enable_timing ||
> + phys->enable_state == DPU_ENC_DISABLED)
> + continue;
I think a more correct way would be to follow the enable path. In
dpu_encoder_virt_atomic_disable() first call disable() for the slave,
then for the master. It should fix your issue.
> +
> + spin_lock_irqsave(phys->enc_spinlock, lock_flags);
> + phys->hw_intf->ops.enable_timing(phys->hw_intf, 0);
> + spin_unlock_irqrestore(phys->enc_spinlock, lock_flags);
> + }
> +}
> +
> static void dpu_encoder_virt_atomic_disable(struct drm_encoder *drm_enc,
> struct drm_atomic_commit *state)
> {
> @@ -1412,6 +1442,9 @@ static void dpu_encoder_virt_atomic_disable(struct drm_encoder *drm_enc,
>
> dpu_encoder_resource_control(drm_enc, DPU_ENC_RC_EVENT_PRE_STOP);
>
> + if (dpu_enc->num_phys_encs > 1)
> + dpu_encoder_stop_slave_timing(dpu_enc);
> +
> for (i = 0; i < dpu_enc->num_phys_encs; i++) {
> struct dpu_encoder_phys *phys = dpu_enc->phys_encs[i];
>
>
> base-commit: 6375e61c01e93e35ee7acd336a689ac1fae4b509
> --
> 2.43.0
>
--
With best wishes
Dmitry