Re: [RFC v7 03/11] drm/vblank: Add vblank works

From: Daniel Vetter
Date: Fri Jun 26 2020 - 17:18:36 EST


On Wed, Jun 24, 2020 at 07:03:10PM -0400, Lyude Paul wrote:
> Add some kind of vblank workers. The interface is similar to regular
> delayed works, and is mostly based off kthread_work. It allows for
> scheduling delayed works that execute once a particular vblank sequence
> has passed. It also allows for accurate flushing of scheduled vblank
> works - in that flushing waits for both the vblank sequence and job
> execution to complete, or for the work to get cancelled - whichever
> comes first.
>
> Whatever hardware programming we do in the work must be fast (must at
> least complete during the vblank or scanout period, sometimes during the
> first few scanlines of the vblank). As such we use a high-priority
> per-CRTC thread to accomplish this.
>
> Changes since v6:
> * Get rid of ->pending and seqcounts, and implement flushing through
> simpler means - danvet
> * Get rid of work_lock, just use drm_device->event_lock
> * Move drm_vblank_work item cleanup into drm_crtc_vblank_off() so that
> we ensure that all vblank work has finished before disabling vblanks
> * Add checks into drm_crtc_vblank_reset() so we yell if it gets called
> while there's vblank workers active
> * Grab event_lock in both drm_crtc_vblank_on()/drm_crtc_vblank_off(),
> the main reason for this is so that other threads calling
> drm_vblank_work_schedule() are blocked from attempting to schedule
> while we're in the middle of enabling/disabling vblanks.
> * Move drm_handle_vblank_works() call below drm_handle_vblank_events()
> * Simplify drm_vblank_work_cancel_sync()
> * Fix drm_vblank_work_cancel_sync() documentation
> * Move wake_up_all() calls out of spinlock where we can. The only one I
> left was the call to wake_up_all() in drm_vblank_handle_works() as
> this seemed like it made more sense just living in that function
> (which is all technically under lock)
> * Move drm_vblank_work related functions into their own source files
> * Add drm_vblank_internal.h so we can export some functions we don't
> want drivers using, but that we do need to use in drm_vblank_work.c
> * Add a bunch of documentation
> Changes since v4:
> * Get rid of kthread interfaces we tried adding and move all of the
> locking into drm_vblank.c. For implementing drm_vblank_work_flush(),
> we now use a wait_queue and sequence counters in order to
> differentiate between multiple work item executions.
> * Get rid of drm_vblank_work_cancel() - this would have been pretty
> difficult to actually reimplement and it occurred to me that neither
> nouveau or i915 are even planning to use this function. Since there's
> also no async cancel function for most of the work interfaces in the
> kernel, it seems a bit unnecessary anyway.
> * Get rid of to_drm_vblank_work() since we now are also able to just
> pass the struct drm_vblank_work to work item callbacks anyway
> Changes since v3:
> * Use our own spinlocks, don't integrate so tightly with kthread_works
> Changes since v2:
> * Use kthread_workers instead of reinventing the wheel.
>
> Cc: Daniel Vetter <daniel@xxxxxxxx>
> Cc: Tejun Heo <tj@xxxxxxxxxx>
> Cc: dri-devel@xxxxxxxxxxxxxxxxxxxxx
> Cc: nouveau@xxxxxxxxxxxxxxxxxxxxx
> Co-developed-by: Ville Syrjälä <ville.syrjala@xxxxxxxxxxxxxxx>
> Signed-off-by: Lyude Paul <lyude@xxxxxxxxxx>

I found a bunch of tiny details below, but overall looks great and thanks
for polishing the kerneldoc.

With the details addressed one way or another:

Reviewed-by: Daniel Vetter <daniel.vetter@xxxxxxxx>

But feel free to resend and poke me again if you want me to recheck the
details that needed changing.

Cheers, Daniel


> ---
> Documentation/gpu/drm-kms.rst | 15 ++
> drivers/gpu/drm/Makefile | 2 +-
> drivers/gpu/drm/drm_vblank.c | 55 +++--
> drivers/gpu/drm/drm_vblank_internal.h | 19 ++
> drivers/gpu/drm/drm_vblank_work.c | 259 +++++++++++++++++++++
> drivers/gpu/drm/drm_vblank_work_internal.h | 24 ++
> include/drm/drm_vblank.h | 20 ++
> include/drm/drm_vblank_work.h | 71 ++++++
> 8 files changed, 447 insertions(+), 18 deletions(-)
> create mode 100644 drivers/gpu/drm/drm_vblank_internal.h
> create mode 100644 drivers/gpu/drm/drm_vblank_work.c
> create mode 100644 drivers/gpu/drm/drm_vblank_work_internal.h
> create mode 100644 include/drm/drm_vblank_work.h
>
> diff --git a/Documentation/gpu/drm-kms.rst b/Documentation/gpu/drm-kms.rst
> index 975cfeb8a3532..3c5ae4f6dfd23 100644
> --- a/Documentation/gpu/drm-kms.rst
> +++ b/Documentation/gpu/drm-kms.rst
> @@ -543,3 +543,18 @@ Vertical Blanking and Interrupt Handling Functions Reference
>
> .. kernel-doc:: drivers/gpu/drm/drm_vblank.c
> :export:
> +
> +Vertical Blank Work
> +===================
> +
> +.. kernel-doc:: drivers/gpu/drm/drm_vblank_work.c
> + :doc: vblank works
> +
> +Vertical Blank Work Functions Reference
> +---------------------------------------
> +
> +.. kernel-doc:: include/drm/drm_vblank_work.h
> + :internal:
> +
> +.. kernel-doc:: drivers/gpu/drm/drm_vblank_work.c
> + :export:
> diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
> index 2c0e5a7e59536..02ee5faf1a925 100644
> --- a/drivers/gpu/drm/Makefile
> +++ b/drivers/gpu/drm/Makefile
> @@ -18,7 +18,7 @@ drm-y := drm_auth.o drm_cache.o \
> drm_dumb_buffers.o drm_mode_config.o drm_vblank.o \
> drm_syncobj.o drm_lease.o drm_writeback.o drm_client.o \
> drm_client_modeset.o drm_atomic_uapi.o drm_hdcp.o \
> - drm_managed.o
> + drm_managed.o drm_vblank_work.o
>
> drm-$(CONFIG_DRM_LEGACY) += drm_legacy_misc.o drm_bufs.o drm_context.o drm_dma.o drm_scatter.o drm_lock.o
> drm-$(CONFIG_DRM_LIB_RANDOM) += lib/drm_random.o
> diff --git a/drivers/gpu/drm/drm_vblank.c b/drivers/gpu/drm/drm_vblank.c
> index e895f5331fdb4..b353bc8328414 100644
> --- a/drivers/gpu/drm/drm_vblank.c
> +++ b/drivers/gpu/drm/drm_vblank.c
> @@ -25,6 +25,7 @@
> */
>
> #include <linux/export.h>
> +#include <linux/kthread.h>
> #include <linux/moduleparam.h>
>
> #include <drm/drm_crtc.h>
> @@ -37,6 +38,8 @@
>
> #include "drm_internal.h"
> #include "drm_trace.h"
> +#include "drm_vblank_internal.h"
> +#include "drm_vblank_work_internal.h"

Feels mild overkill to have these files with 1-2 functions each, I'd stuff
them all into drm_internal.h. We do have other vblank stuff in there
already.

>
> /**
> * DOC: vblank handling
> @@ -363,7 +366,7 @@ static void drm_update_vblank_count(struct drm_device *dev, unsigned int pipe,
> store_vblank(dev, pipe, diff, t_vblank, cur_vblank);
> }
>
> -static u64 drm_vblank_count(struct drm_device *dev, unsigned int pipe)
> +u64 drm_vblank_count(struct drm_device *dev, unsigned int pipe)
> {
> struct drm_vblank_crtc *vblank = &dev->vblank[pipe];
> u64 count;
> @@ -497,6 +500,7 @@ static void drm_vblank_init_release(struct drm_device *dev, void *ptr)
> drm_WARN_ON(dev, READ_ONCE(vblank->enabled) &&
> drm_core_check_feature(dev, DRIVER_MODESET));
>
> + drm_vblank_destroy_worker(vblank);
> del_timer_sync(&vblank->disable_timer);
> }
>
> @@ -539,6 +543,10 @@ int drm_vblank_init(struct drm_device *dev, unsigned int num_crtcs)
> vblank);
> if (ret)
> return ret;
> +
> + ret = drm_vblank_worker_init(vblank);
> + if (ret)
> + return ret;
> }
>
> return 0;
> @@ -1135,7 +1143,7 @@ static int drm_vblank_enable(struct drm_device *dev, unsigned int pipe)
> return ret;
> }
>
> -static int drm_vblank_get(struct drm_device *dev, unsigned int pipe)
> +int drm_vblank_get(struct drm_device *dev, unsigned int pipe)
> {
> struct drm_vblank_crtc *vblank = &dev->vblank[pipe];
> unsigned long irqflags;
> @@ -1178,7 +1186,7 @@ int drm_crtc_vblank_get(struct drm_crtc *crtc)
> }
> EXPORT_SYMBOL(drm_crtc_vblank_get);
>
> -static void drm_vblank_put(struct drm_device *dev, unsigned int pipe)
> +void drm_vblank_put(struct drm_device *dev, unsigned int pipe)
> {
> struct drm_vblank_crtc *vblank = &dev->vblank[pipe];
>
> @@ -1281,13 +1289,16 @@ void drm_crtc_vblank_off(struct drm_crtc *crtc)
> unsigned int pipe = drm_crtc_index(crtc);
> struct drm_vblank_crtc *vblank = &dev->vblank[pipe];
> struct drm_pending_vblank_event *e, *t;
> -
> ktime_t now;
> u64 seq;
>
> if (drm_WARN_ON(dev, pipe >= dev->num_crtcs))
> return;
>
> + /*
> + * Grab event_lock early to prevent vblank work from being scheduled
> + * while we're in the middle of shutting down vblank interrupts
> + */
> spin_lock_irq(&dev->event_lock);
>
> spin_lock(&dev->vbl_lock);
> @@ -1324,11 +1335,18 @@ void drm_crtc_vblank_off(struct drm_crtc *crtc)
> drm_vblank_put(dev, pipe);
> send_vblank_event(dev, e, seq, now);
> }
> +
> + /* Cancel any leftover pending vblank work */
> + drm_vblank_cancel_pending_works(vblank);
> +
> spin_unlock_irq(&dev->event_lock);
>
> /* Will be reset by the modeset helpers when re-enabling the crtc by
> * calling drm_calc_timestamping_constants(). */
> vblank->hwmode.crtc_clock = 0;
> +
> + /* Wait for any vblank work that's still executing to finish */
> + drm_vblank_flush_worker(vblank);
> }
> EXPORT_SYMBOL(drm_crtc_vblank_off);
>
> @@ -1363,6 +1381,7 @@ void drm_crtc_vblank_reset(struct drm_crtc *crtc)
> spin_unlock_irqrestore(&dev->vbl_lock, irqflags);
>
> drm_WARN_ON(dev, !list_empty(&dev->vblank_event_list));
> + drm_WARN_ON(dev, !list_empty(&vblank->pending_work));
> }
> EXPORT_SYMBOL(drm_crtc_vblank_reset);
>
> @@ -1417,7 +1436,10 @@ void drm_crtc_vblank_on(struct drm_crtc *crtc)
> if (drm_WARN_ON(dev, pipe >= dev->num_crtcs))
> return;
>
> - spin_lock_irqsave(&dev->vbl_lock, irqflags);
> + /* So vblank works can't be scheduled until we've finished */
> + spin_lock_irqsave(&dev->event_lock, irqflags);

This smells fishy, why do we need this? drm_enable_vblank takes the
->vblank_time_lock spinlock, which is the first thing drm_handle_vblank
takes, so there's absolute no way for a vblank event or worker to get
ahead of this here.

Except if I'm missing something this isn't needed.

> +
> + spin_lock(&dev->vbl_lock);
> drm_dbg_vbl(dev, "crtc %d, vblank enabled %d, inmodeset %d\n",
> pipe, vblank->enabled, vblank->inmodeset);
>
> @@ -1435,7 +1457,9 @@ void drm_crtc_vblank_on(struct drm_crtc *crtc)
> */
> if (atomic_read(&vblank->refcount) != 0 || drm_vblank_offdelay == 0)
> drm_WARN_ON(dev, drm_vblank_enable(dev, pipe));
> - spin_unlock_irqrestore(&dev->vbl_lock, irqflags);
> +
> + spin_unlock(&dev->vbl_lock);
> + spin_unlock_irqrestore(&dev->event_lock, irqflags);
> }
> EXPORT_SYMBOL(drm_crtc_vblank_on);
>
> @@ -1589,11 +1613,6 @@ int drm_legacy_modeset_ctl_ioctl(struct drm_device *dev, void *data,
> return 0;
> }
>
> -static inline bool vblank_passed(u64 seq, u64 ref)
> -{
> - return (seq - ref) <= (1 << 23);
> -}
> -
> static int drm_queue_vblank_event(struct drm_device *dev, unsigned int pipe,
> u64 req_seq,
> union drm_wait_vblank *vblwait,
> @@ -1650,7 +1669,7 @@ static int drm_queue_vblank_event(struct drm_device *dev, unsigned int pipe,
> trace_drm_vblank_event_queued(file_priv, pipe, req_seq);
>
> e->sequence = req_seq;
> - if (vblank_passed(seq, req_seq)) {
> + if (drm_vblank_passed(seq, req_seq)) {
> drm_vblank_put(dev, pipe);
> send_vblank_event(dev, e, seq, now);
> vblwait->reply.sequence = seq;
> @@ -1805,7 +1824,7 @@ int drm_wait_vblank_ioctl(struct drm_device *dev, void *data,
> }
>
> if ((flags & _DRM_VBLANK_NEXTONMISS) &&
> - vblank_passed(seq, req_seq)) {
> + drm_vblank_passed(seq, req_seq)) {
> req_seq = seq + 1;
> vblwait->request.type &= ~_DRM_VBLANK_NEXTONMISS;
> vblwait->request.sequence = req_seq;
> @@ -1824,7 +1843,7 @@ int drm_wait_vblank_ioctl(struct drm_device *dev, void *data,
> drm_dbg_core(dev, "waiting on vblank count %llu, crtc %u\n",
> req_seq, pipe);
> wait = wait_event_interruptible_timeout(vblank->queue,
> - vblank_passed(drm_vblank_count(dev, pipe), req_seq) ||
> + drm_vblank_passed(drm_vblank_count(dev, pipe), req_seq) ||
> !READ_ONCE(vblank->enabled),
> msecs_to_jiffies(3000));
>
> @@ -1873,7 +1892,7 @@ static void drm_handle_vblank_events(struct drm_device *dev, unsigned int pipe)
> list_for_each_entry_safe(e, t, &dev->vblank_event_list, base.link) {
> if (e->pipe != pipe)
> continue;
> - if (!vblank_passed(seq, e->sequence))
> + if (!drm_vblank_passed(seq, e->sequence))
> continue;
>
> drm_dbg_core(dev, "vblank event on %llu, current %llu\n",
> @@ -1943,6 +1962,7 @@ bool drm_handle_vblank(struct drm_device *dev, unsigned int pipe)
> !atomic_read(&vblank->refcount));
>
> drm_handle_vblank_events(dev, pipe);
> + drm_handle_vblank_works(vblank);
>
> spin_unlock_irqrestore(&dev->event_lock, irqflags);
>
> @@ -2096,7 +2116,7 @@ int drm_crtc_queue_sequence_ioctl(struct drm_device *dev, void *data,
> if (flags & DRM_CRTC_SEQUENCE_RELATIVE)
> req_seq += seq;
>
> - if ((flags & DRM_CRTC_SEQUENCE_NEXT_ON_MISS) && vblank_passed(seq, req_seq))
> + if ((flags & DRM_CRTC_SEQUENCE_NEXT_ON_MISS) && drm_vblank_passed(seq, req_seq))
> req_seq = seq + 1;
>
> e->pipe = pipe;
> @@ -2125,7 +2145,7 @@ int drm_crtc_queue_sequence_ioctl(struct drm_device *dev, void *data,
>
> e->sequence = req_seq;
>
> - if (vblank_passed(seq, req_seq)) {
> + if (drm_vblank_passed(seq, req_seq)) {
> drm_crtc_vblank_put(crtc);
> send_vblank_event(dev, e, seq, now);
> queue_seq->sequence = seq;
> @@ -2145,3 +2165,4 @@ int drm_crtc_queue_sequence_ioctl(struct drm_device *dev, void *data,
> kfree(e);
> return ret;
> }
> +
> diff --git a/drivers/gpu/drm/drm_vblank_internal.h b/drivers/gpu/drm/drm_vblank_internal.h
> new file mode 100644
> index 0000000000000..217ae5442ddce
> --- /dev/null
> +++ b/drivers/gpu/drm/drm_vblank_internal.h
> @@ -0,0 +1,19 @@
> +// SPDX-License-Identifier: MIT
> +
> +#ifndef DRM_VBLANK_INTERNAL_H
> +#define DRM_VBLANK_INTERNAL_H
> +
> +#include <linux/types.h>
> +
> +#include <drm/drm_device.h>
> +
> +static inline bool drm_vblank_passed(u64 seq, u64 ref)
> +{
> + return (seq - ref) <= (1 << 23);
> +}
> +
> +int drm_vblank_get(struct drm_device *dev, unsigned int pipe);
> +void drm_vblank_put(struct drm_device *dev, unsigned int pipe);
> +u64 drm_vblank_count(struct drm_device *dev, unsigned int pipe);
> +
> +#endif /* !DRM_VBLANK_INTERNAL_H */
> diff --git a/drivers/gpu/drm/drm_vblank_work.c b/drivers/gpu/drm/drm_vblank_work.c
> new file mode 100644
> index 0000000000000..0762ad34cdcc0
> --- /dev/null
> +++ b/drivers/gpu/drm/drm_vblank_work.c
> @@ -0,0 +1,259 @@
> +// SPDX-License-Identifier: MIT
> +
> +#include <uapi/linux/sched/types.h>
> +
> +#include <drm/drm_print.h>
> +#include <drm/drm_vblank.h>
> +#include <drm/drm_vblank_work.h>
> +#include <drm/drm_crtc.h>
> +
> +#include "drm_vblank_internal.h"
> +#include "drm_vblank_work_internal.h"
> +
> +/**
> + * DOC: vblank works
> + *
> + * Many DRM drivers need to program hardware in a time-sensitive manner, many
> + * times with a deadline of starting and finishing within a certain region of
> + * the scanout. Most of the time the safest way to accomplish this is to
> + * simply do said time-sensitive programming in the driver's IRQ handler,
> + * which allows drivers to avoid being preempted during these critical
> + * regions. Or even better, the hardware may even handle applying such
> + * time-critical programming independently of the CPU.
> + *
> + * While there's a decent amount of hardware that's designed so that the CPU
> + * doesn't need to be concerned with extremely time-sensitive programming,
> + * there's a few situations where it can't be helped. Some unforgiving
> + * hardware may require that certain time-sensitive programming be handled
> + * completely by the CPU, and said programming may even take too long to
> + * handle in an IRQ handler. Another such situation would be where the driver
> + * needs to perform a task that needs to complete within a specific scanout
> + * period, but might possibly block and thus cannot be handled in an IRQ
> + * context. Both of these situations can't be solved perfectly in Linux since
> + * we're not a realtime kernel, and thus the scheduler may cause us to miss
> + * our deadline if it decides to preempt us. But for some drivers, it's good
> + * enough if we can lower our chance of being preempted to an absolute
> + * minimum.
> + *
> + * This is where &drm_vblank_work comes in. &drm_vblank_work provides a simple
> + * generic delayed work implementation which delays work execution until a
> + * particular vblank has passed, and then executes the work at realtime
> + * priority. This provides the best possible chance at performing
> + * time-sensitive hardware programming on time, even when the system is under
> + * heavy load. &drm_vblank_work also supports rescheduling, so that self
> + * re-arming work items can be easily implemented.
> + */
> +
> +void drm_handle_vblank_works(struct drm_vblank_crtc *vblank)
> +{
> + struct drm_vblank_work *work, *next;
> + u64 count = atomic64_read(&vblank->count);
> + bool wake = false;
> +
> + assert_spin_locked(&vblank->dev->event_lock);
> +
> + list_for_each_entry_safe(work, next, &vblank->pending_work, node) {
> + if (!drm_vblank_passed(count, work->count))
> + continue;
> +
> + list_del_init(&work->node);
> + drm_vblank_put(vblank->dev, vblank->pipe);
> + kthread_queue_work(vblank->worker, &work->base);
> + wake = true;
> + }
> + if (wake)
> + wake_up_all(&vblank->work_wait_queue);
> +}
> +
> +/* Handle cancelling any pending vblank work items and drop respective vblank
> + * references in response to vblank interrupts being disabled.
> + */
> +void drm_vblank_cancel_pending_works(struct drm_vblank_crtc *vblank)
> +{
> + struct drm_vblank_work *work, *next;
> +
> + assert_spin_locked(&vblank->dev->event_lock);
> +
> + list_for_each_entry_safe(work, next, &vblank->pending_work, node) {
> + list_del_init(&work->node);
> + drm_vblank_put(vblank->dev, vblank->pipe);
> + }
> +
> + wake_up_all(&vblank->work_wait_queue);
> +}
> +
> +/**
> + * drm_vblank_work_schedule - schedule a vblank work
> + * @work: vblank work to schedule
> + * @count: target vblank count
> + * @nextonmiss: defer until the next vblank if target vblank was missed
> + *
> + * Schedule @work for execution once the crtc vblank count reaches @count.
> + *
> + * If the crtc vblank count has already reached @count and @nextonmiss is
> + * %false the work starts to execute immediately.
> + *
> + * If the crtc vblank count has already reached @count and @nextonmiss is
> + * %true the work is deferred until the next vblank (as if @count has been
> + * specified as crtc vblank count + 1).
> + *
> + * If @work is already scheduled, this function will reschedule said work
> + * using the new @count.

Maybe clarify here that "This can be use for self-rearming work items." or
something like that.

> + *
> + * Returns:
> + * 0 on success, error code on failure.
> + */
> +int drm_vblank_work_schedule(struct drm_vblank_work *work,
> + u64 count, bool nextonmiss)
> +{
> + struct drm_vblank_crtc *vblank = work->vblank;
> + struct drm_device *dev = vblank->dev;
> + u64 cur_vbl;
> + unsigned long irqflags;
> + bool passed, rescheduling = false, wake = false;
> + int ret = 0;
> +
> + spin_lock_irqsave(&dev->event_lock, irqflags);
> + if (!vblank->worker || vblank->inmodeset || work->cancelling)

Oh nice catch with ->inmodeset, I totally missed to check re-arming vs
drm_crtc_vblank_off races. Only problem I'm seeing is that we're holding
the wrong spinlock, this needs to be check under ->vbl_lock. But
->cancelling needs the event_lock, so I think you need to split this check
into two, and grab the ->vbl_lock around the ->inmodeset check.

The ->worker check otoh looks fishy, that should never happen. If you feel
like some defensive programming then I think that should be an

if (WARN_ON(!vblank->worker))
return;


> + goto out;
> +
> + if (list_empty(&work->node)) {
> + ret = drm_vblank_get(dev, vblank->pipe);

Ok that kills the idea of converting the _irqsave to _irq in
drm_vblank_get. I do wonder whether it wouldn't be nicer to have the
vblank_get outside of the spinlock, and unconditional - would allow you to
drop the ->inmodeset check. But the end result in code flow cleanliness is
not any better, so not a good idea I think.

> + if (ret < 0)
> + goto out;
> + } else if (work->count == count) {
> + /* Already scheduled w/ same vbl count */
> + goto out;
> + } else {
> + rescheduling = true;
> + }
> +
> + work->count = count;
> + cur_vbl = drm_vblank_count(dev, vblank->pipe);
> + passed = drm_vblank_passed(cur_vbl, count);
> + if (passed)
> + DRM_DEV_ERROR(dev->dev,
> + "crtc %d vblank %llu already passed (current %llu)\n",
> + vblank->pipe, count, cur_vbl);

This is a bit loud, I think that should be debug out most. You can't
really prevent races. I do wonder though whether we should do something
like 1 indicates that the work item has been scheduled, and 0 that it
hasn't been scheduled (aside from failure, which is negative).

> +
> + if (!nextonmiss && passed) {
> + drm_vblank_put(dev, vblank->pipe);
> + kthread_queue_work(vblank->worker, &work->base);
> +
> + if (rescheduling) {
> + list_del_init(&work->node);
> + wake = true;
> + }
> + } else if (!rescheduling) {
> + list_add_tail(&work->node, &vblank->pending_work);
> + }
> +
> +out:
> + spin_unlock_irqrestore(&dev->event_lock, irqflags);
> + if (wake)
> + wake_up_all(&vblank->work_wait_queue);
> + return ret;
> +}
> +EXPORT_SYMBOL(drm_vblank_work_schedule);

I think the above control flow is all correct, but this is the kind of
stuff that's prime material for some selftests. But we don't have enough
ready-made mocking I think, so not going to ask for that. Just an idea.

> +
> +/**
> + * drm_vblank_work_cancel_sync - cancel a vblank work and wait for it to
> + * finish executing
> + * @work: vblank work to cancel
> + *
> + * Cancel an already scheduled vblank work and wait for its
> + * execution to finish.
> + *
> + * On return, @work is guaranteed to no longer be scheduled or running, even
> + * if it's self-arming.
> + *
> + * Returns:
> + * %True if the work was cancelled before it started to execute, %false
> + * otherwise.
> + */
> +bool drm_vblank_work_cancel_sync(struct drm_vblank_work *work)
> +{
> + struct drm_vblank_crtc *vblank = work->vblank;
> + struct drm_device *dev = vblank->dev;
> + bool ret = false;
> +
> + spin_lock_irq(&dev->event_lock);
> + if (!list_empty(&work->node)) {
> + list_del_init(&work->node);
> + drm_vblank_put(vblank->dev, vblank->pipe);
> + ret = true;
> + }
> +
> + work->cancelling++;
> + spin_unlock_irq(&dev->event_lock);
> +
> + wake_up_all(&vblank->work_wait_queue);
> +
> + if (kthread_cancel_work_sync(&work->base))
> + ret = true;
> +
> + spin_lock_irq(&dev->event_lock);
> + work->cancelling--;
> + spin_unlock_irq(&dev->event_lock);

lgtm, everything looks ordered correctly to avoid a self-arming work
escaping.

> +
> + return ret;
> +}
> +EXPORT_SYMBOL(drm_vblank_work_cancel_sync);
> +
> +/**
> + * drm_vblank_work_flush - wait for a scheduled vblank work to finish
> + * executing
> + * @work: vblank work to flush
> + *
> + * Wait until @work has finished executing once.
> + */
> +void drm_vblank_work_flush(struct drm_vblank_work *work)
> +{
> + struct drm_vblank_crtc *vblank = work->vblank;
> + struct drm_device *dev = vblank->dev;
> +
> + spin_lock_irq(&dev->event_lock);
> + wait_event_lock_irq(vblank->work_wait_queue, list_empty(&work->node),
> + dev->event_lock);
> + spin_unlock_irq(&dev->event_lock);
> +
> + kthread_flush_work(&work->base);

So much less magic here, I like.

> +}
> +EXPORT_SYMBOL(drm_vblank_work_flush);
> +
> +/**
> + * drm_vblank_work_init - initialize a vblank work item
> + * @work: vblank work item
> + * @crtc: CRTC whose vblank will trigger the work execution
> + * @func: work function to be executed
> + *
> + * Initialize a vblank work item for a specific crtc.
> + */
> +void drm_vblank_work_init(struct drm_vblank_work *work, struct drm_crtc *crtc,
> + void (*func)(struct kthread_work *work))
> +{
> + kthread_init_work(&work->base, func);
> + INIT_LIST_HEAD(&work->node);
> + work->vblank = &crtc->dev->vblank[drm_crtc_index(crtc)];
> +}
> +EXPORT_SYMBOL(drm_vblank_work_init);
> +
> +int drm_vblank_worker_init(struct drm_vblank_crtc *vblank)
> +{
> + struct sched_param param = {
> + .sched_priority = MAX_RT_PRIO - 1,
> + };
> + struct kthread_worker *worker;
> +
> + INIT_LIST_HEAD(&vblank->pending_work);
> + init_waitqueue_head(&vblank->work_wait_queue);
> + worker = kthread_create_worker(0, "card%d-crtc%d",
> + vblank->dev->primary->index,
> + vblank->pipe);
> + if (IS_ERR(worker))
> + return PTR_ERR(worker);
> +
> + vblank->worker = worker;
> +
> + return sched_setscheduler(vblank->worker->task, SCHED_FIFO, &param);
> +}
> diff --git a/drivers/gpu/drm/drm_vblank_work_internal.h b/drivers/gpu/drm/drm_vblank_work_internal.h
> new file mode 100644
> index 0000000000000..0a4abbc4ab295
> --- /dev/null
> +++ b/drivers/gpu/drm/drm_vblank_work_internal.h
> @@ -0,0 +1,24 @@
> +// SPDX-License-Identifier: MIT
> +
> +#ifndef _DRM_VBLANK_WORK_INTERNAL_H_
> +#define _DRM_VBLANK_WORK_INTERNAL_H_
> +
> +#include <drm/drm_vblank.h>
> +
> +int drm_vblank_worker_init(struct drm_vblank_crtc *vblank);
> +void drm_vblank_cancel_pending_works(struct drm_vblank_crtc *vblank);
> +void drm_handle_vblank_works(struct drm_vblank_crtc *vblank);
> +
> +static inline void drm_vblank_flush_worker(struct drm_vblank_crtc *vblank)
> +{
> + if (vblank->worker)

Is this check really required? We should always have a worker I thought?

> + kthread_flush_worker(vblank->worker);
> +}
> +
> +static inline void drm_vblank_destroy_worker(struct drm_vblank_crtc *vblank)
> +{
> + if (vblank->worker)

Same here.

> + kthread_destroy_worker(vblank->worker);
> +}
> +
> +#endif /* !_DRM_VBLANK_WORK_INTERNAL_H_ */
> diff --git a/include/drm/drm_vblank.h b/include/drm/drm_vblank.h
> index dd9f5b9e56e4e..dd125f8c766cf 100644
> --- a/include/drm/drm_vblank.h
> +++ b/include/drm/drm_vblank.h
> @@ -27,12 +27,14 @@
> #include <linux/seqlock.h>
> #include <linux/idr.h>
> #include <linux/poll.h>
> +#include <linux/kthread.h>
>
> #include <drm/drm_file.h>
> #include <drm/drm_modes.h>
>
> struct drm_device;
> struct drm_crtc;
> +struct drm_vblank_work;
>
> /**
> * struct drm_pending_vblank_event - pending vblank event tracking
> @@ -203,6 +205,24 @@ struct drm_vblank_crtc {
> * disabling functions multiple times.
> */
> bool enabled;
> +
> + /**
> + * @worker: The &kthread_worker used for executing vblank works.
> + */
> + struct kthread_worker *worker;
> +
> + /**
> + * @pending_work: A list of scheduled &drm_vblank_work items that are
> + * waiting for a future vblank.
> + */
> + struct list_head pending_work;
> +
> + /**
> + * @work_wait_queue: The wait queue used for signaling that a
> + * &drm_vblank_work item has either finished executing, or was
> + * cancelled.
> + */
> + wait_queue_head_t work_wait_queue;
> };
>
> int drm_vblank_init(struct drm_device *dev, unsigned int num_crtcs);
> diff --git a/include/drm/drm_vblank_work.h b/include/drm/drm_vblank_work.h
> new file mode 100644
> index 0000000000000..f0439c039f7ce
> --- /dev/null
> +++ b/include/drm/drm_vblank_work.h
> @@ -0,0 +1,71 @@
> +// SPDX-License-Identifier: MIT
> +
> +#ifndef _DRM_VBLANK_WORK_H_
> +#define _DRM_VBLANK_WORK_H_
> +
> +#include <linux/kthread.h>
> +
> +struct drm_crtc;
> +
> +/**
> + * struct drm_vblank_work - A delayed work item which delays until a target
> + * vblank passes, and then executes at realtime priority outside of IRQ
> + * context.
> + *
> + * See also:
> + * drm_vblank_work_schedule()
> + * drm_vblank_work_init()
> + * drm_vblank_work_cancel_sync()
> + * drm_vblank_work_flush()
> + */
> +struct drm_vblank_work {
> + /**
> + * @base: The base &kthread_work item which will be executed by
> + * &drm_vblank_crtc.worker. Drivers should not interact with this
> + * directly, and instead rely on drm_vblank_work_init() to initialize
> + * this.
> + */
> + struct kthread_work base;
> +
> + /**
> + * @vblank: A pointer to &drm_vblank_crtc this work item belongs to.
> + */
> + struct drm_vblank_crtc *vblank;
> +
> + /**
> + * @count: The target vblank this work will execute on. Drivers should
> + * not modify this value directly, and instead use
> + * drm_vblank_work_schedule()
> + */
> + u64 count;
> +
> + /**
> + * @cancelling: The number of drm_vblank_work_cancel_sync() calls that
> + * are currently running. A work item cannot be rescheduled until all
> + * calls have finished.
> + */
> + int cancelling;
> +
> + /**
> + * @node: The position of this work item in
> + * &drm_vblank_crtc.pending_work.
> + */
> + struct list_head node;
> +};
> +
> +/**
> + * to_drm_vblank_work - Retrieve the respective &drm_vblank_work item from a
> + * &kthread_work
> + * @_work: The &kthread_work embedded inside a &drm_vblank_work
> + */
> +#define to_drm_vblank_work(_work) \
> + container_of((_work), struct drm_vblank_work, base)
> +
> +int drm_vblank_work_schedule(struct drm_vblank_work *work,
> + u64 count, bool nextonmiss);
> +void drm_vblank_work_init(struct drm_vblank_work *work, struct drm_crtc *crtc,
> + void (*func)(struct kthread_work *work));
> +bool drm_vblank_work_cancel_sync(struct drm_vblank_work *work);
> +void drm_vblank_work_flush(struct drm_vblank_work *work);
> +
> +#endif /* !_DRM_VBLANK_WORK_H_ */
> --
> 2.26.2
>

--
Daniel Vetter
Software Engineer, Intel Corporation
http://blog.ffwll.ch