diff mbox series

[4/4] drm/imx: only send commit done event when all state has been applied

Message ID 20180914165917.8464-4-l.stach@pengutronix.de (mailing list archive)
State New, archived
Headers show
Series [1/4] gpu: ipu-v3: pre: add double buffer status readback | expand

Commit Message

Lucas Stach Sept. 14, 2018, 4:59 p.m. UTC
Currently there is a small race window where we could manage to arm the
vblank event from atomic flush, but programming the hardware was too close
to the frame end, so the hardware will only apply the current state on the
next vblank. In this case we will send out the commit done event too early
causing userspace to reuse framebuffes that are still in use.

Instead of using the event arming mechnism, just remember the pending event
and send it from the vblank IRQ handler, once we are sure that all state
has been applied sucessfully.

Signed-off-by: Lucas Stach <l.stach@pengutronix.de>
---
 drivers/gpu/drm/imx/ipuv3-crtc.c | 32 ++++++++++++++++++++++++++++----
 1 file changed, 28 insertions(+), 4 deletions(-)

Comments

Philipp Zabel Oct. 5, 2018, 3:11 p.m. UTC | #1
On Fri, 2018-09-14 at 18:59 +0200, Lucas Stach wrote:
> Currently there is a small race window where we could manage to arm the
> vblank event from atomic flush, but programming the hardware was too close
> to the frame end, so the hardware will only apply the current state on the
> next vblank. In this case we will send out the commit done event too early
> causing userspace to reuse framebuffes that are still in use.
> 
> Instead of using the event arming mechnism, just remember the pending event
> and send it from the vblank IRQ handler, once we are sure that all state
> has been applied sucessfully.
> 
> Signed-off-by: Lucas Stach <l.stach@pengutronix.de>
> ---
>  drivers/gpu/drm/imx/ipuv3-crtc.c | 32 ++++++++++++++++++++++++++++----
>  1 file changed, 28 insertions(+), 4 deletions(-)
> 
> diff --git a/drivers/gpu/drm/imx/ipuv3-crtc.c b/drivers/gpu/drm/imx/ipuv3-crtc.c
> index 7d4b710b837a..b0c95565a28d 100644
> --- a/drivers/gpu/drm/imx/ipuv3-crtc.c
> +++ b/drivers/gpu/drm/imx/ipuv3-crtc.c
> @@ -42,6 +42,7 @@ struct ipu_crtc {
>  	struct ipu_dc		*dc;
>  	struct ipu_di		*di;
>  	int			irq;
> +	struct drm_pending_vblank_event *event;
>  };
>  
>  static inline struct ipu_crtc *to_ipu_crtc(struct drm_crtc *crtc)
> @@ -181,8 +182,31 @@ static const struct drm_crtc_funcs ipu_crtc_funcs = {
>  static irqreturn_t ipu_irq_handler(int irq, void *dev_id)
>  {
>  	struct ipu_crtc *ipu_crtc = dev_id;
> +	struct drm_crtc *crtc = &ipu_crtc->base;
> +	unsigned long flags;
> +	int i;
> +
> +	drm_crtc_handle_vblank(crtc);
> +
> +	if (ipu_crtc->event) {
> +		for (i = 0; i < ARRAY_SIZE(ipu_crtc->plane); i++) {
> +			struct ipu_plane *plane = ipu_crtc->plane[i];
> +
> +			if (!plane)
> +				continue;
> +
> +			if (!ipu_plane_atomic_update_done(&plane->base))

			if (ipu_plane_atomic_update_pending(&plane->base))

> +				break;
> +		}
>  
> -	drm_crtc_handle_vblank(&ipu_crtc->base);
> +		if (i == ARRAY_SIZE(ipu_crtc->plane)) {
> +			spin_lock_irqsave(&crtc->dev->event_lock, flags);
> +			drm_crtc_send_vblank_event(crtc, ipu_crtc->event);
> +			ipu_crtc->event = NULL;

These two happen under the event spinlock, but where event is set in
ipu_crtc_atomic_flush, the locking is removed.

> +			drm_crtc_vblank_put(crtc);
> +			spin_unlock_irqrestore(&crtc->dev->event_lock, flags);
> +		}
> +	}
>  
>  	return IRQ_HANDLED;
>  }
> @@ -229,13 +253,13 @@ static void ipu_crtc_atomic_begin(struct drm_crtc *crtc,
>  static void ipu_crtc_atomic_flush(struct drm_crtc *crtc,
>  				  struct drm_crtc_state *old_crtc_state)
>  {
> -	spin_lock_irq(&crtc->dev->event_lock);
> +	struct ipu_crtc *ipu_crtc = to_ipu_crtc(crtc);
> +
>  	if (crtc->state->event) {
>  		WARN_ON(drm_crtc_vblank_get(crtc));
> -		drm_crtc_arm_vblank_event(crtc, crtc->state->event);
> +		ipu_crtc->event = crtc->state->event;

We assume here that ipu_crtc->event is NULL and the irq handler is never
running at the same time, otherwise we would drop an event. This is non-
obvious to me, and I think it warrants a comment.

My understanding is the following:

- It is virtually impossible for atomic_flush to race against a delayed
  previous ipu_irq_handler because the previous commit's commit_tail
  would still be waiting for the vblank event to release it from
  drm_atomic_helper_wait_for_flip_done.

  However, if the last commit's tail finishes after the irq_handler
  calls drm_crtc_send_vblank_event(), and the new commit is issued, and
  its tail work scheduled, all before the next line in the irq_handler,
  ipu_crtc->event = NULL, then the new commit's tail could call
  drm_atomic_helper_commit_planes and therefore ipu_crtc_atomic_flush
  and ipu_crtc->event would be overwritten.

- It is unproblematic for a delayed atomic_flush to race against the
  next ipu_irq_handler because ipu_crtc->event will just not be set
  when the irq handler checks it, and the vblank event will be deferred
  to the next interrupt.

regards
Philipp
Philipp Zabel Jan. 23, 2019, 11:35 a.m. UTC | #2
On Fri, 2018-10-05 at 17:11 +0200, Philipp Zabel wrote:
> On Fri, 2018-09-14 at 18:59 +0200, Lucas Stach wrote:
> > Currently there is a small race window where we could manage to arm the
> > vblank event from atomic flush, but programming the hardware was too close
> > to the frame end, so the hardware will only apply the current state on the
> > next vblank. In this case we will send out the commit done event too early
> > causing userspace to reuse framebuffes that are still in use.
> > 
> > Instead of using the event arming mechnism, just remember the pending event
> > and send it from the vblank IRQ handler, once we are sure that all state
> > has been applied sucessfully.
> > 
> > Signed-off-by: Lucas Stach <l.stach@pengutronix.de>
> > ---
> >  drivers/gpu/drm/imx/ipuv3-crtc.c | 32 ++++++++++++++++++++++++++++----
> >  1 file changed, 28 insertions(+), 4 deletions(-)
> > 
> > diff --git a/drivers/gpu/drm/imx/ipuv3-crtc.c b/drivers/gpu/drm/imx/ipuv3-crtc.c
> > index 7d4b710b837a..b0c95565a28d 100644
> > --- a/drivers/gpu/drm/imx/ipuv3-crtc.c
> > +++ b/drivers/gpu/drm/imx/ipuv3-crtc.c
> > @@ -42,6 +42,7 @@ struct ipu_crtc {
> >  	struct ipu_dc		*dc;
> >  	struct ipu_di		*di;
> >  	int			irq;
> > +	struct drm_pending_vblank_event *event;
> >  };
> >  
> >  static inline struct ipu_crtc *to_ipu_crtc(struct drm_crtc *crtc)
> > @@ -181,8 +182,31 @@ static const struct drm_crtc_funcs ipu_crtc_funcs = {
> >  static irqreturn_t ipu_irq_handler(int irq, void *dev_id)
> >  {
> >  	struct ipu_crtc *ipu_crtc = dev_id;
> > +	struct drm_crtc *crtc = &ipu_crtc->base;
> > +	unsigned long flags;
> > +	int i;
> > +
> > +	drm_crtc_handle_vblank(crtc);
> > +
> > +	if (ipu_crtc->event) {
> > +		for (i = 0; i < ARRAY_SIZE(ipu_crtc->plane); i++) {
> > +			struct ipu_plane *plane = ipu_crtc->plane[i];
> > +
> > +			if (!plane)
> > +				continue;
> > +
> > +			if (!ipu_plane_atomic_update_done(&plane->base))
> 
> 			if (ipu_plane_atomic_update_pending(&plane->base))
> 
> > +				break;
> > +		}
> >  
> > -	drm_crtc_handle_vblank(&ipu_crtc->base);
> > +		if (i == ARRAY_SIZE(ipu_crtc->plane)) {
> > +			spin_lock_irqsave(&crtc->dev->event_lock, flags);
> > +			drm_crtc_send_vblank_event(crtc, ipu_crtc->event);
> > +			ipu_crtc->event = NULL;
> 
> These two happen under the event spinlock, but where event is set in
> ipu_crtc_atomic_flush, the locking is removed.
> 
> > +			drm_crtc_vblank_put(crtc);
> > +			spin_unlock_irqrestore(&crtc->dev->event_lock, flags);
> > +		}
> > +	}
> >  
> >  	return IRQ_HANDLED;
> >  }
> > @@ -229,13 +253,13 @@ static void ipu_crtc_atomic_begin(struct drm_crtc *crtc,
> >  static void ipu_crtc_atomic_flush(struct drm_crtc *crtc,
> >  				  struct drm_crtc_state *old_crtc_state)
> >  {
> > -	spin_lock_irq(&crtc->dev->event_lock);
> > +	struct ipu_crtc *ipu_crtc = to_ipu_crtc(crtc);
> > +
> >  	if (crtc->state->event) {
> >  		WARN_ON(drm_crtc_vblank_get(crtc));
> > -		drm_crtc_arm_vblank_event(crtc, crtc->state->event);
> > +		ipu_crtc->event = crtc->state->event;
> 
> We assume here that ipu_crtc->event is NULL and the irq handler is never
> running at the same time, otherwise we would drop an event. This is non-
> obvious to me, and I think it warrants a comment.
> 
> My understanding is the following:
> 
> - It is virtually impossible for atomic_flush to race against a delayed
>   previous ipu_irq_handler because the previous commit's commit_tail
>   would still be waiting for the vblank event to release it from
>   drm_atomic_helper_wait_for_flip_done.
> 
>   However, if the last commit's tail finishes after the irq_handler
>   calls drm_crtc_send_vblank_event(), and the new commit is issued, and
>   its tail work scheduled, all before the next line in the irq_handler,
>   ipu_crtc->event = NULL, then the new commit's tail could call
>   drm_atomic_helper_commit_planes and therefore ipu_crtc_atomic_flush
>   and ipu_crtc->event would be overwritten.
> 
> - It is unproblematic for a delayed atomic_flush to race against the
>   next ipu_irq_handler because ipu_crtc->event will just not be set
>   when the irq handler checks it, and the vblank event will be deferred
>   to the next interrupt.

How do we proceed with this? Keep the spin lock?

regards
Philipp
Daniel Vetter Jan. 23, 2019, 1:04 p.m. UTC | #3
On Wed, Jan 23, 2019 at 12:35:02PM +0100, Philipp Zabel wrote:
> On Fri, 2018-10-05 at 17:11 +0200, Philipp Zabel wrote:
> > On Fri, 2018-09-14 at 18:59 +0200, Lucas Stach wrote:
> > > Currently there is a small race window where we could manage to arm the
> > > vblank event from atomic flush, but programming the hardware was too close
> > > to the frame end, so the hardware will only apply the current state on the
> > > next vblank. In this case we will send out the commit done event too early
> > > causing userspace to reuse framebuffes that are still in use.
> > > 
> > > Instead of using the event arming mechnism, just remember the pending event
> > > and send it from the vblank IRQ handler, once we are sure that all state
> > > has been applied sucessfully.
> > > 
> > > Signed-off-by: Lucas Stach <l.stach@pengutronix.de>
> > > ---
> > >  drivers/gpu/drm/imx/ipuv3-crtc.c | 32 ++++++++++++++++++++++++++++----
> > >  1 file changed, 28 insertions(+), 4 deletions(-)
> > > 
> > > diff --git a/drivers/gpu/drm/imx/ipuv3-crtc.c b/drivers/gpu/drm/imx/ipuv3-crtc.c
> > > index 7d4b710b837a..b0c95565a28d 100644
> > > --- a/drivers/gpu/drm/imx/ipuv3-crtc.c
> > > +++ b/drivers/gpu/drm/imx/ipuv3-crtc.c
> > > @@ -42,6 +42,7 @@ struct ipu_crtc {
> > >  	struct ipu_dc		*dc;
> > >  	struct ipu_di		*di;
> > >  	int			irq;
> > > +	struct drm_pending_vblank_event *event;
> > >  };
> > >  
> > >  static inline struct ipu_crtc *to_ipu_crtc(struct drm_crtc *crtc)
> > > @@ -181,8 +182,31 @@ static const struct drm_crtc_funcs ipu_crtc_funcs = {
> > >  static irqreturn_t ipu_irq_handler(int irq, void *dev_id)
> > >  {
> > >  	struct ipu_crtc *ipu_crtc = dev_id;
> > > +	struct drm_crtc *crtc = &ipu_crtc->base;
> > > +	unsigned long flags;
> > > +	int i;
> > > +
> > > +	drm_crtc_handle_vblank(crtc);
> > > +
> > > +	if (ipu_crtc->event) {
> > > +		for (i = 0; i < ARRAY_SIZE(ipu_crtc->plane); i++) {
> > > +			struct ipu_plane *plane = ipu_crtc->plane[i];
> > > +
> > > +			if (!plane)
> > > +				continue;
> > > +
> > > +			if (!ipu_plane_atomic_update_done(&plane->base))
> > 
> > 			if (ipu_plane_atomic_update_pending(&plane->base))
> > 
> > > +				break;
> > > +		}
> > >  
> > > -	drm_crtc_handle_vblank(&ipu_crtc->base);
> > > +		if (i == ARRAY_SIZE(ipu_crtc->plane)) {
> > > +			spin_lock_irqsave(&crtc->dev->event_lock, flags);
> > > +			drm_crtc_send_vblank_event(crtc, ipu_crtc->event);
> > > +			ipu_crtc->event = NULL;
> > 
> > These two happen under the event spinlock, but where event is set in
> > ipu_crtc_atomic_flush, the locking is removed.
> > 
> > > +			drm_crtc_vblank_put(crtc);
> > > +			spin_unlock_irqrestore(&crtc->dev->event_lock, flags);
> > > +		}
> > > +	}
> > >  
> > >  	return IRQ_HANDLED;
> > >  }
> > > @@ -229,13 +253,13 @@ static void ipu_crtc_atomic_begin(struct drm_crtc *crtc,
> > >  static void ipu_crtc_atomic_flush(struct drm_crtc *crtc,
> > >  				  struct drm_crtc_state *old_crtc_state)
> > >  {
> > > -	spin_lock_irq(&crtc->dev->event_lock);
> > > +	struct ipu_crtc *ipu_crtc = to_ipu_crtc(crtc);
> > > +
> > >  	if (crtc->state->event) {
> > >  		WARN_ON(drm_crtc_vblank_get(crtc));
> > > -		drm_crtc_arm_vblank_event(crtc, crtc->state->event);
> > > +		ipu_crtc->event = crtc->state->event;
> > 
> > We assume here that ipu_crtc->event is NULL and the irq handler is never
> > running at the same time, otherwise we would drop an event. This is non-
> > obvious to me, and I think it warrants a comment.
> > 
> > My understanding is the following:
> > 
> > - It is virtually impossible for atomic_flush to race against a delayed
> >   previous ipu_irq_handler because the previous commit's commit_tail
> >   would still be waiting for the vblank event to release it from
> >   drm_atomic_helper_wait_for_flip_done.
> > 
> >   However, if the last commit's tail finishes after the irq_handler
> >   calls drm_crtc_send_vblank_event(), and the new commit is issued, and
> >   its tail work scheduled, all before the next line in the irq_handler,
> >   ipu_crtc->event = NULL, then the new commit's tail could call
> >   drm_atomic_helper_commit_planes and therefore ipu_crtc_atomic_flush
> >   and ipu_crtc->event would be overwritten.
> > 
> > - It is unproblematic for a delayed atomic_flush to race against the
> >   next ipu_irq_handler because ipu_crtc->event will just not be set
> >   when the irq handler checks it, and the vblank event will be deferred
> >   to the next interrupt.
> 
> How do we proceed with this? Keep the spin lock?

Yeah, standard practice is to protect these things with a spinlock,
usually the drm->event_lock. Then the flip_done wait should make sure
overall ordering is correct, too.

Might be good to improve the kerneldocs that this is a recommended
pattern.
-Daniel
diff mbox series

Patch

diff --git a/drivers/gpu/drm/imx/ipuv3-crtc.c b/drivers/gpu/drm/imx/ipuv3-crtc.c
index 7d4b710b837a..b0c95565a28d 100644
--- a/drivers/gpu/drm/imx/ipuv3-crtc.c
+++ b/drivers/gpu/drm/imx/ipuv3-crtc.c
@@ -42,6 +42,7 @@  struct ipu_crtc {
 	struct ipu_dc		*dc;
 	struct ipu_di		*di;
 	int			irq;
+	struct drm_pending_vblank_event *event;
 };
 
 static inline struct ipu_crtc *to_ipu_crtc(struct drm_crtc *crtc)
@@ -181,8 +182,31 @@  static const struct drm_crtc_funcs ipu_crtc_funcs = {
 static irqreturn_t ipu_irq_handler(int irq, void *dev_id)
 {
 	struct ipu_crtc *ipu_crtc = dev_id;
+	struct drm_crtc *crtc = &ipu_crtc->base;
+	unsigned long flags;
+	int i;
+
+	drm_crtc_handle_vblank(crtc);
+
+	if (ipu_crtc->event) {
+		for (i = 0; i < ARRAY_SIZE(ipu_crtc->plane); i++) {
+			struct ipu_plane *plane = ipu_crtc->plane[i];
+
+			if (!plane)
+				continue;
+
+			if (!ipu_plane_atomic_update_done(&plane->base))
+				break;
+		}
 
-	drm_crtc_handle_vblank(&ipu_crtc->base);
+		if (i == ARRAY_SIZE(ipu_crtc->plane)) {
+			spin_lock_irqsave(&crtc->dev->event_lock, flags);
+			drm_crtc_send_vblank_event(crtc, ipu_crtc->event);
+			ipu_crtc->event = NULL;
+			drm_crtc_vblank_put(crtc);
+			spin_unlock_irqrestore(&crtc->dev->event_lock, flags);
+		}
+	}
 
 	return IRQ_HANDLED;
 }
@@ -229,13 +253,13 @@  static void ipu_crtc_atomic_begin(struct drm_crtc *crtc,
 static void ipu_crtc_atomic_flush(struct drm_crtc *crtc,
 				  struct drm_crtc_state *old_crtc_state)
 {
-	spin_lock_irq(&crtc->dev->event_lock);
+	struct ipu_crtc *ipu_crtc = to_ipu_crtc(crtc);
+
 	if (crtc->state->event) {
 		WARN_ON(drm_crtc_vblank_get(crtc));
-		drm_crtc_arm_vblank_event(crtc, crtc->state->event);
+		ipu_crtc->event = crtc->state->event;
 		crtc->state->event = NULL;
 	}
-	spin_unlock_irq(&crtc->dev->event_lock);
 }
 
 static void ipu_crtc_mode_set_nofb(struct drm_crtc *crtc)