Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.
notro edited this page Jul 11, 2013 · 10 revisions

Frame rate is most often expressed in frames per second (FPS).
In the motion picture industry, where traditional film stock is used, the industry standard filming and projection formats are 24 frames per second. Historically, 25fps was used in some European countries.

FBTFT uses the term fps in two instances:

  • As an argument to fbtft_device
    This should in my earlier understanding be the maximum speed at which the driver would update the display.
    Default is fps=20.
  • In debug output (DEBUG_TIME_FIRST_UPDATE and DEBUG_TIME_EACH_UPDATE)
    This generates a message in the kernel log like this:
    hy28afb spi0.0: Elapsed time for display update: 43.530004 ms (fps: 22, lines=240)
    FBTFT measures the time for a display update and divides that by the video memory size to get the fps value.
    It is really only fps if the whole display is updated, and not only a portion of it.

FBTFT is based on st7735fb.c by Matt Porter. Both use Framebuffer Deferred IO.
This means that when an application mmap's /dev/fb1 and starts writing to video memory, this generates a page fault and schedules a display update to happen after a preset time. During this time, the application continues writing to memory. When the display update happens, all pages in video memory that have changed, will be written to the display.

This scenario works fine on slow displays that can't keep up with all the changes.

There is however a problem here. The first time the application writes to video memory, a display update is scheduled to happen 1/20 second later. When that time arrives, the list of changed pages is locked during the display update. If that update takes 1/20 seconds, the next update will happen 1/20 seconds after that.

Thus we get: app's first write -> wait 1/20 s -> update display 1/20 s -> pending page faults comes in -> wait 1/20 s -> update display ...

Showing a video will in reality give us half the fps I were expecting, because the Deferred IO system is locked during display_update.

One solution to this problem could be to use a fixed display update schedule. Every 1/20 second the driver checks for changes, and writes if neccesary.

I have to ponder this some more.

Another issue, is that above fps=20 we only have 4 distinct values: 25, 26, 34, 51 (explained further down).

Detailed walkthrough

This is a detailed look at the update_display codepath in FBTFT.

jiffies

The kernel keeps track of the flow of time by means of timer interrupts. Every time a timer interrupt occurs, the value of an internal kernel counter, jiffies, is incremented.

The macro HZ defines the number of jiffies in one second

File: /arch/arm/include/asm/param.h

# define HZ		CONFIG_HZ	/* Internal kernel timer frequency */
File: .config
// default in Raspian
CONFIG_HZ=100

HZ defines the resolution of fps:

	fbdefio->delay =           HZ/fps;

Thus above fps=20 we only have 4 distinct values: 100/25=4, 100/26=3, 100/34=2, 100/51=1 (integer division)
FBTFT writes the "real" fps to the kernel log when the framebuffer is registered.

Data structures

File: include/linux/fb.h

struct fb_info {
...
#ifdef CONFIG_FB_DEFERRED_IO
	struct delayed_work deferred_work;
	struct fb_deferred_io *fbdefio;
#endif
...
};

#ifdef CONFIG_FB_DEFERRED_IO
struct fb_deferred_io {
	/* delay between mkwrite and deferred handler */
	unsigned long delay;
	struct mutex lock; /* mutex that protects the page list */
	struct list_head pagelist; /* list of touched pages */
	/* callback */
	void (*first_io)(struct fb_info *info);
	void (*deferred_io)(struct fb_info *info, struct list_head *pagelist);
};
#endif
File: include/linux/workqueue.h

struct delayed_work {
        struct work_struct work;
        struct timer_list timer;

        /* target workqueue and CPU ->timer uses to queue ->work */
        struct workqueue_struct *wq;
        int cpu;
};

Initialization codepath

Driver calls fbtft_framebuffer_alloc()

File: adafruit22fb.c
static int adafruit22fb_probe(struct spi_device *spi)
...
	info = fbtft_framebuffer_alloc(&adafruit22_display, &spi->dev);
File: fbtft-core.c
struct fb_info *fbtft_framebuffer_alloc(struct fbtft_display *display, struct device *dev)
...
	unsigned fps = display->fps;
...
	/* defaults */
	if (!fps)
		fps = 20;
...
	/* platform_data override ? */
	if (pdata) {
		if (pdata->fps)
			fps = pdata->fps;
...
	fbdefio = kzalloc(sizeof(struct fb_deferred_io), GFP_KERNEL);
...
	info->fbdefio = fbdefio;
...
	fbdefio->delay =           HZ/fps;
	fbdefio->deferred_io =     fbtft_deferred_io;
	fb_deferred_io_init(info);
File: drivers/video/fb_defio.c
void fb_deferred_io_init(struct fb_info *info)
{
	struct fb_deferred_io *fbdefio = info->fbdefio;

	BUG_ON(!fbdefio);
	mutex_init(&fbdefio->lock);
	info->fbops->fb_mmap = fb_deferred_io_mmap;
	INIT_DELAYED_WORK(&info->deferred_work, fb_deferred_io_work);
	INIT_LIST_HEAD(&fbdefio->pagelist);
	if (fbdefio->delay == 0) /* set a default of 1 s */
		fbdefio->delay = HZ;
}
File: include/linux/workqueue.h
// INIT_DELAYED_WORK(&info->deferred_work, fb_deferred_io_work)
#define INIT_DELAYED_WORK(_work, _func)					\
	__INIT_DELAYED_WORK(_work, _func, 0)
// __INIT_DELAYED_WORK(&info->deferred_work, fb_deferred_io_work, 0)
#define __INIT_DELAYED_WORK(_work, _func, _tflags)			\
	do {								\
		INIT_WORK(&(_work)->work, (_func));			\
		__setup_timer(&(_work)->timer, delayed_work_timer_fn,	\
			      (unsigned long)(_work),			\
			      (_tflags) | TIMER_IRQSAFE);		\
	} while (0)
// INIT_WORK(&info->deferred_work->work, fb_deferred_io_work);
#define INIT_WORK(_work, _func)						\
	do {								\
		__INIT_WORK((_work), (_func), 0);			\
	} while (0)
// __INIT_WORK(&info->deferred_work->work, fb_deferred_io_work, 0);
#define __INIT_WORK(_work, _func, _onstack)				\
	do {								\
		__init_work((_work), _onstack);				\
		(_work)->data = (atomic_long_t) WORK_DATA_INIT();	\
		INIT_LIST_HEAD(&(_work)->entry);			\
		PREPARE_WORK((_work), (_func));				\
	} while (0)
File: kernel/workqueue.c

// __init_work(&info->deferred_work->work, 0);
void __init_work(struct work_struct *work, int onstack)
{
	if (onstack)
		debug_object_init_on_stack(work, &work_debug_descr);
	else
		debug_object_init(work, &work_debug_descr);
}
// PREPARE_WORK(&info->deferred_work->work, fb_deferred_io_work);
/*
 * initialize a work item's function pointer
 */
#define PREPARE_WORK(_work, _func)					\
	do {								\
		(_work)->func = (_func);				\
	} while (0)
File: include/linux/timer.h

// __setup_timer(&info->deferred_work->timer, delayed_work_timer_fn, (unsigned long)(&info->deferred_work), TIMER_IRQSAFE);
#define __setup_timer(_timer, _fn, _data, _flags)			\
	do {								\
		__init_timer((_timer), (_flags));			\
		(_timer)->function = (_fn);				\
		(_timer)->data = (_data);				\
	} while (0)

Display update codepath

Action: Application mmap's /dev/fb1

File: drivers/video/fb_defio.c

static const struct vm_operations_struct fb_deferred_io_vm_ops = {
	.fault		= fb_deferred_io_fault,
	.page_mkwrite	= fb_deferred_io_mkwrite,
};

static int fb_deferred_io_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
	vma->vm_ops = &fb_deferred_io_vm_ops;
	vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;
	if (!(info->flags & FBINFO_VIRTFB))
		vma->vm_flags |= VM_IO;
	vma->vm_private_data = info;
	return 0;
}

Action: Application writes to videomemory

/* vm_ops->page_mkwrite handler */
static int fb_deferred_io_mkwrite(struct vm_area_struct *vma,
				  struct vm_fault *vmf)
{
	struct page *page = vmf->page;
	struct fb_info *info = vma->vm_private_data;
	struct fb_deferred_io *fbdefio = info->fbdefio;
	struct page *cur;

	/* this is a callback we get when userspace first tries to
	write to the page. we schedule a workqueue. that workqueue
	will eventually mkclean the touched pages and execute the
	deferred framebuffer IO. then if userspace touches a page
	again, we repeat the same scheme */

	file_update_time(vma->vm_file);

	/* protect against the workqueue changing the page list */
	mutex_lock(&fbdefio->lock);

	/* first write in this cycle, notify the driver */
	if (fbdefio->first_io && list_empty(&fbdefio->pagelist))
		fbdefio->first_io(info);

	/*
	 * We want the page to remain locked from ->page_mkwrite until
	 * the PTE is marked dirty to avoid page_mkclean() being called
	 * before the PTE is updated, which would leave the page ignored
	 * by defio.
	 * Do this by locking the page here and informing the caller
	 * about it with VM_FAULT_LOCKED.
	 */
	lock_page(page);

	/* we loop through the pagelist before adding in order
	to keep the pagelist sorted */
	list_for_each_entry(cur, &fbdefio->pagelist, lru) {
		/* this check is to catch the case where a new
		process could start writing to the same page
		through a new pte. this new access can cause the
		mkwrite even when the original ps's pte is marked
		writable */
		if (unlikely(cur == page))
			goto page_already_added;
		else if (cur->index > page->index)
			break;
	}

	list_add_tail(&page->lru, &cur->lru);

page_already_added:
	mutex_unlock(&fbdefio->lock);

	/* come back after delay to process the deferred IO */
	schedule_delayed_work(&info->deferred_work, fbdefio->delay);
	return VM_FAULT_LOCKED;
}
File: include/linux/workqueue.h

// schedule_delayed_work(&info->deferred_work, fbdefio->delay);
/**
 * schedule_delayed_work - put work task in global workqueue after delay
 * @dwork: job to be done
 * @delay: number of jiffies to wait or 0 for immediate execution
 *
 * After waiting for a given time this puts a job in the kernel-global
 * workqueue.
 */
static inline bool schedule_delayed_work(struct delayed_work *dwork,
					 unsigned long delay)
{
	return queue_delayed_work(system_wq, dwork, delay);
}
// queue_delayed_work(system_wq, &info->deferred_work, fbdefio->delay);
/**
 * queue_delayed_work - queue work on a workqueue after delay
 * @wq: workqueue to use
 * @dwork: delayable work to queue
 * @delay: number of jiffies to wait before queueing
 *
 * Equivalent to queue_delayed_work_on() but tries to use the local CPU.
 */
static inline bool queue_delayed_work(struct workqueue_struct *wq,
				      struct delayed_work *dwork,
				      unsigned long delay)
{
	return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
}
File: kernel/workqueue.c

// queue_delayed_work_on(WORK_CPU_UNBOUND, system_wq, &info->deferred_work, fbdefio->delay);
/**
 * queue_delayed_work_on - queue work on specific CPU after delay
 * @cpu: CPU number to execute work on
 * @wq: workqueue to use
 * @dwork: work to queue
 * @delay: number of jiffies to wait before queueing
 *
 * Returns %false if @work was already on a queue, %true otherwise.  If
 * @delay is zero and @dwork is idle, it will be scheduled for immediate
 * execution.
 */
bool queue_delayed_work_on(int cpu, struct workqueue_struct *wq,
			   struct delayed_work *dwork, unsigned long delay)
{
	struct work_struct *work = &dwork->work;
	bool ret = false;
	unsigned long flags;

	/* read the comment in __queue_work() */
	local_irq_save(flags);

	if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
		__queue_delayed_work(cpu, wq, dwork, delay);
		ret = true;
	}

	local_irq_restore(flags);
	return ret;
}
// __queue_delayed_work(WORK_CPU_UNBOUND, system_wq, &info->deferred_work, fbdefio->delay);
static void __queue_delayed_work(int cpu, struct workqueue_struct *wq,
				struct delayed_work *dwork, unsigned long delay)
{
	struct timer_list *timer = &dwork->timer;
	struct work_struct *work = &dwork->work;

	WARN_ON_ONCE(timer->function != delayed_work_timer_fn ||
		     timer->data != (unsigned long)dwork);
	WARN_ON_ONCE(timer_pending(timer));
	WARN_ON_ONCE(!list_empty(&work->entry));

	/*
	 * If @delay is 0, queue @dwork->work immediately.  This is for
	 * both optimization and correctness.  The earliest @timer can
	 * expire is on the closest next tick and delayed_work users depend
	 * on that there's no such delay when @delay is 0.
	 */
	if (!delay) {
		__queue_work(cpu, wq, &dwork->work);
		return;
	}

	timer_stats_timer_set_start_info(&dwork->timer);

	dwork->wq = wq;
	dwork->cpu = cpu;
	timer->expires = jiffies + delay;

	if (unlikely(cpu != WORK_CPU_UNBOUND))
		add_timer_on(timer, cpu);
	else
		add_timer(timer);
}
File: kernel/timer.c

// add_timer(&info->deferred_work->timer);
/**
 * add_timer - start a timer
 * @timer: the timer to be added
 *
 * The kernel will do a ->function(->data) callback from the
 * timer interrupt at the ->expires point in the future. The
 * current time is 'jiffies'.
 *
 * The timer's ->expires, ->function (and if the handler uses it, ->data)
 * fields must be set prior calling this function.
 *
 * Timers with an ->expires field in the past will be executed in the next
 * timer tick.
 */
void add_timer(struct timer_list *timer)
{
	BUG_ON(timer_pending(timer));
	mod_timer(timer, timer->expires);
}

When the timer fires

File: kernel/workqueue.c

void delayed_work_timer_fn(unsigned long __data)
{
        struct delayed_work *dwork = (struct delayed_work *)__data;

        /* should have been called from irqsafe timer with irq already off */
        __queue_work(dwork->cpu, dwork->wq, &dwork->work);
}
File: drivers/video/fb_defio.c

/* workqueue callback */
static void fb_deferred_io_work(struct work_struct *work)
{
	struct fb_info *info = container_of(work, struct fb_info,
						deferred_work.work);
	struct list_head *node, *next;
	struct page *cur;
	struct fb_deferred_io *fbdefio = info->fbdefio;

	/* here we mkclean the pages, then do all deferred IO */
	mutex_lock(&fbdefio->lock);
	list_for_each_entry(cur, &fbdefio->pagelist, lru) {
		lock_page(cur);
		page_mkclean(cur);
		unlock_page(cur);
	}

	/* driver's callback with pagelist */
	fbdefio->deferred_io(info, &fbdefio->pagelist);

	/* clear the list */
	list_for_each_safe(node, next, &fbdefio->pagelist) {
		list_del(node);
	}
	mutex_unlock(&fbdefio->lock);
}
File: fbtft-core.c

void fbtft_deferred_io(struct fb_info *info, struct list_head *pagelist)
{
	struct fbtft_par *par = info->par;
	struct page *page;
	unsigned long index;
	unsigned y_low=0, y_high=0;
	int count = 0;

	/* debug can be changed via sysfs */
	fbtft_debug_sync_value(par);

	/* Mark display lines as dirty */
	list_for_each_entry(page, pagelist, lru) {
		count++;
		index = page->index << PAGE_SHIFT;
		y_low = index / info->fix.line_length;
		y_high = (index + PAGE_SIZE - 1) / info->fix.line_length;
		fbtft_fbtft_dev_dbg(DEBUG_DEFERRED_IO, par, info->device, "page->index=%lu y_low=%d y_high=%d\n", page->index, y_low, y_high);
		if (y_high > info->var.yres - 1)
			y_high = info->var.yres - 1;
		if (y_low < par->dirty_lines_start)
			par->dirty_lines_start = y_low;
		if (y_high > par->dirty_lines_end)
			par->dirty_lines_end = y_high;
	}

	par->fbtftops.update_display(info->par);
}

References

piwik

Clone this wiki locally