Patch Framebuffer Driver of PrimeCell Color LCD Controller with Double-Buffering to Support Android’s Page-Flipping

Brief introduction

Android uses a screen composition engine called SurfaceFlinger. It takes the input of Surface objects from each Windows, then writes the output to the frame buffer. Each Surface is double-buffered. The front buffer is taken by SurfaceFlinger for composition, while the back buffer is used to perform drawing. Once the drawing is completed, Android does a page-flipping by changing the offset value on Y axis of the frame buffer to switch them. During the initialization of EGLDisplaySurface, if the virtual resolution on the Y axis of the frame buffer is found out to be bigger than the real resolution of the Y axis, then the frame buffer is used directly for double-buffering. Otherwise, the back buffer is simply copied to the front buffer, which causes delay in buffer-swapping. Thus double-buffering would better to be supported by the frame buffer driver for the sake of performance.

Double-buffering is supported in the goldfish frame buffer driver, which is used by the QEMU-based emulator shipped with Android SDK. I happen to be working on porting Android to an ARM RealView board. The board has PL111 PrimeCell Color LCD Controller, which is AMBA compliant SoC peripheral. By comparing the source code of goldfish frame buffer driver (from Android cupcake branch source tree) and that of the ARM LCD controller frame buffer driver (from 2.6.27 vanilla kernel), one can easily figure out what should be patched in the ARM LCD frame buffer driver (kernel/drivers/video/amba-clcd.c) to support double-buffering. Firstly, in the initialization of the driver, change to “fb->fb.fix.ypanstep = 1;” and “fb->fb.var.yres_virtual = fb->panel->mode.yres * 2;” Then in the declaration of “clcdfb_ops”, add this line of code “.fb_pan_display = clcdfb_pan_display,”. Also define this new function “clcdfb_pan_display” by putting an invocation to the function “clcdfb_set_start”. In the definition of the function “clcdfb_check” in kernel/include/linux/amba/clcd.h, change from “var->yres_virtual = var->yres = (var->yres + 1) & ~1;” to “var->yres = (var->yres + 1) & ~1; var->yres_virtual = var->yres * 2;” since the checking happens to reset the value of yres_virtual to that of yres. One more step required is to increase the frame size to needed. In kernel/arch/arm/mach-realview/core.c, change from “static unsigned long framesize = SZ_1M;” to “static unsigned long framesize = 0x12C000;”. 0x12C000 is the value of 640 * 480 * 2 * 2 (VGA, 16bpp and double-buffer), because VGA is the largest display resolution that is provided by PL111 and can still support double-buffering due to the limited display memory.

How page-flipping works

– In frameworks/base/libs/ui/EGLDisplaySurface.cpp, function “EGLDisplaySurface::swapBuffers()” does the real work by calling ioctl of FBIOPUT_VSCREENINFO
– In “kernel/drivers/video/fbmem.c”, function “fb_ioctl” handles over the work in “case FBIOPUT_VSCREENINFO:”, then “fb_set_var” is invoked. In that function, both “fbops->fb_set_par” and “fb_pan_display” are invoked. Similarly, in “fb_pan_display”, “fbops->fb_pan_display” is also invoked. “fbops->fb_set_par” and “fbops->fb_pan_display” mentioned above are provided by the amba clcd frame buffer driver in kernel/drivers/video/amba-clcd.c.
– According to the conventions, “fb_set_par” implementation in the frame buffer driver is supposed to react to possible changes of screen orientation and resolution. And “fb_pan_display” is supposed to re-map the frame buffer as the offset changes in X or Y axis. In this way, page-flipping is properly handled.

Problem of “clcdfb_set_par”

In “clcdfb_set_par” of the current amba clcd implementation, it turns off the power and clock source of the frame buffer, sets clocks and the buffer base, then it turns on the frame buffer. This is not applied to Android because the page-flipping gonna invoke FBIOPUT_VSCREENINFO ioctl so often that “clcdfb_set_par” is going to be invoked a lot as well. Then the whole system is not operatable at all as a result. It’s obviously that at least the implementation of “clcdfb_set_par” is not written in a way that adheres to the conventions. Or writting frame buffer implementation in this case still requires keeping the usage of Android in mind.

The solution to that is to cache the frame buffer settings. Each time “clcd_set_par” is invoked, the new frame buffer settings are compared with the cached ones. If the screen orientation, resolution and other changes that will require reset of clock don’t happen, then simply skip any operations; otherwise, the new frame buffer settings are cached and the clock is reseted.

The actual patch

The patch to the said files above is illustrated below:


diff -Naur kernel-2.6.27.orig/arch/arm/mach-realview/core.c kernel-2.6.27/arch/arm/mach-realview/core.c
--- kernel-2.6.27.orig/arch/arm/mach-realview/core.c	2009-04-29 16:16:29.000000000 +0200
+++ kernel-2.6.27/arch/arm/mach-realview/core.c	2009-04-29 16:12:26.000000000 +0200
@@ -358,7 +358,8 @@
writel(val, sys_clcd);
}

-static unsigned long framesize = SZ_1M;
+/* 640*480*2*2 (VGA, 16bpp and double-buffer) required by Android */
+static unsigned long framesize = 0x12C000;

static int realview_clcd_setup(struct clcd_fb *fb)
{
diff -Naur kernel-2.6.27.orig/drivers/video/amba-clcd.c kernel-2.6.27/drivers/video/amba-clcd.c
--- kernel-2.6.27.orig/drivers/video/amba-clcd.c	2009-04-29 16:16:58.000000000 +0200
+++ kernel-2.6.27/drivers/video/amba-clcd.c	2009-04-29 16:13:16.000000000 +0200
@@ -194,6 +194,37 @@
return ret;
}

+struct fb_var_screeninfo cached_fb_var;
+int is_fb_var_cached = 0;
+
+static int clcdfb_is_fb_changed(struct clcd_fb *fb)
+{
+    if (!is_fb_var_cached ||
+        fb->fb.var.xres != cached_fb_var.xres ||
+        fb->fb.var.yres != cached_fb_var.yres ||
+        fb->fb.var.xres_virtual != cached_fb_var.xres_virtual ||
+        fb->fb.var.yres_virtual != cached_fb_var.yres_virtual ||
+        fb->fb.var.bits_per_pixel != cached_fb_var.bits_per_pixel ||
+        fb->fb.var.grayscale != cached_fb_var.grayscale ||
+        fb->fb.var.green.length != cached_fb_var.green.length ||
+        fb->fb.var.left_margin != cached_fb_var.left_margin ||
+        fb->fb.var.right_margin != cached_fb_var.right_margin ||
+        fb->fb.var.upper_margin != cached_fb_var.upper_margin ||
+        fb->fb.var.lower_margin != cached_fb_var.lower_margin ||
+        fb->fb.var.hsync_len != cached_fb_var.hsync_len ||
+        fb->fb.var.vsync_len != cached_fb_var.vsync_len ||
+        fb->fb.var.sync != cached_fb_var.sync ||
+        fb->fb.var.rotate != cached_fb_var.rotate) {
+
+        cached_fb_var = fb->fb.var;
+        is_fb_var_cached = 1;
+
+        return 1;
+    }
+    else
+        return 0;
+}
+
static int clcdfb_set_par(struct fb_info *info)
{
struct clcd_fb *fb = to_clcd(info);
@@ -207,22 +238,25 @@
else
fb->fb.fix.visual = FB_VISUAL_TRUECOLOR;

-	fb->board->decode(fb, &regs);
+    if (clcdfb_is_fb_changed(fb)) {
+
+	    fb->board->decode(fb, &regs);

-	clcdfb_disable(fb);
+	    clcdfb_disable(fb);

-	writel(regs.tim0, fb->regs + CLCD_TIM0);
-	writel(regs.tim1, fb->regs + CLCD_TIM1);
-	writel(regs.tim2, fb->regs + CLCD_TIM2);
-	writel(regs.tim3, fb->regs + CLCD_TIM3);
+	    writel(regs.tim0, fb->regs + CLCD_TIM0);
+	    writel(regs.tim1, fb->regs + CLCD_TIM1);
+	    writel(regs.tim2, fb->regs + CLCD_TIM2);
+	    writel(regs.tim3, fb->regs + CLCD_TIM3);

-	clcdfb_set_start(fb);
+	    clcdfb_set_start(fb);

-	clk_set_rate(fb->clk, (1000000000 / regs.pixclock) * 1000);
+	    clk_set_rate(fb->clk, (1000000000 / regs.pixclock) * 1000);

-	fb->clcd_cntl = regs.cntl;
+	    fb->clcd_cntl = regs.cntl;

-	clcdfb_enable(fb, regs.cntl);
+	    clcdfb_enable(fb, regs.cntl);
+    }

#ifdef DEBUG
printk(KERN_INFO "CLCD: Registers set to\n"
@@ -289,6 +323,17 @@
return regno > 255;
}

+static int clcdfb_pan_display(struct fb_var_screeninfo *var, struct fb_info *info)
+{
+    struct clcd_fb *fb = NULL;
+
+    info->var = *var;
+	fb = to_clcd(info);
+	clcdfb_set_start(fb);
+
+	return 0;
+}
+
/*
*  Blank the screen if blank_mode != 0, else unblank. If blank == NULL
*  then the caller blanks by setting the CLUT (Color Look Up Table) to all
@@ -332,6 +377,7 @@
.fb_check_var	= clcdfb_check_var,
.fb_set_par	= clcdfb_set_par,
.fb_setcolreg	= clcdfb_setcolreg,
+	.fb_pan_display = clcdfb_pan_display,
.fb_blank	= clcdfb_blank,
.fb_fillrect	= cfb_fillrect,
.fb_copyarea	= cfb_copyarea,
@@ -367,14 +413,14 @@
fb->fb.fix.type		= FB_TYPE_PACKED_PIXELS;
fb->fb.fix.type_aux	= 0;
fb->fb.fix.xpanstep	= 0;
-	fb->fb.fix.ypanstep	= 0;
+	fb->fb.fix.ypanstep	= 1;
fb->fb.fix.ywrapstep	= 0;
fb->fb.fix.accel	= FB_ACCEL_NONE;
fb->fb.var.xres		= fb->panel->mode.xres;
fb->fb.var.yres		= fb->panel->mode.yres;
fb->fb.var.xres_virtual	= fb->panel->mode.xres;
-	fb->fb.var.yres_virtual	= fb->panel->mode.yres;
+	fb->fb.var.yres_virtual	= fb->panel->mode.yres * 2;
fb->fb.var.bits_per_pixel = fb->panel->bpp;
fb->fb.var.grayscale	= fb->panel->grayscale;
fb->fb.var.pixclock	= fb->panel->mode.pixclock;
diff -Naur kernel-2.6.27.orig/include/linux/amba/clcd.h kernel-2.6.27/include/linux/amba/clcd.h
--- kernel-2.6.27.orig/include/linux/amba/clcd.h	2009-04-29 16:16:20.000000000 +0200
+++ kernel-2.6.27/include/linux/amba/clcd.h	2009-04-29 16:12:53.000000000 +0200
@@ -232,7 +232,9 @@
static inline int clcdfb_check(struct clcd_fb *fb, struct fb_var_screeninfo *var)
{
var->xres_virtual = var->xres = (var->xres + 15) & ~15;
-	var->yres_virtual = var->yres = (var->yres + 1) & ~1;
+	//var->yres_virtual = var->yres = (var->yres + 1) & ~1;
+	var->yres = (var->yres + 1) & ~1;
+	var->yres_virtual = var->yres * 2;

#define CHECK(e,l,h) (var->e < l || var->e > h)
if (CHECK(right_margin, (5+1), 256) ||	/* back porch */

Comments are closed.