diff --git a/configure b/configure
index 1654413..c385753 100755
--- a/configure
+++ b/configure
@@ -158,6 +158,7 @@ fdt=""
 nptl=""
 pixman=""
 sdl=""
+fbdev="no"
 virtfs=""
 vnc="yes"
 sparse="no"
@@ -544,6 +545,7 @@ Haiku)
   kvm="yes"
   vhost_net="yes"
   vhost_scsi="yes"
+  fbdev="yes"
   if [ "$cpu" = "i386" -o "$cpu" = "x86_64" ] ; then
     audio_possible_drivers="$audio_possible_drivers fmod"
   fi
@@ -693,6 +695,10 @@ for opt do
   ;;
   --enable-qom-cast-debug) qom_cast_debug="yes"
   ;;
+  --disable-fbdev) fbdev="no"
+  ;;
+  --enable-fbdev) fbdev="yes"
+  ;;
   --disable-virtfs) virtfs="no"
   ;;
   --enable-virtfs) virtfs="yes"
@@ -1044,6 +1050,8 @@ echo "  --disable-sdl            disable SDL"
 echo "  --enable-sdl             enable SDL"
 echo "  --disable-gtk            disable gtk UI"
 echo "  --enable-gtk             enable gtk UI"
+echo "  --disable-fbdev          disable linux framebuffer"
+echo "  --enable-fbdev           enable linux framebuffer"
 echo "  --disable-virtfs         disable VirtFS"
 echo "  --enable-virtfs          enable VirtFS"
 echo "  --disable-vnc            disable VNC"
@@ -3485,6 +3493,7 @@ fi
 echo "pixman            $pixman"
 echo "SDL support       $sdl"
 echo "GTK support       $gtk"
+echo "fbdev support     $fbdev"
 echo "curses support    $curses"
 echo "curl support      $curl"
 echo "mingw32 support   $mingw32"
@@ -3716,6 +3725,9 @@ if test "$sdl" = "yes" ; then
   echo "CONFIG_SDL=y" >> $config_host_mak
   echo "SDL_CFLAGS=$sdl_cflags" >> $config_host_mak
 fi
+if test "$fbdev" = "yes" ; then
+  echo "CONFIG_FBDEV=y" >> $config_host_mak
+fi
 if test "$cocoa" = "yes" ; then
   echo "CONFIG_COCOA=y" >> $config_host_mak
 fi
diff --git a/include/sysemu/sysemu.h b/include/sysemu/sysemu.h
index 2fb71af..5922311 100644
--- a/include/sysemu/sysemu.h
+++ b/include/sysemu/sysemu.h
@@ -91,6 +91,7 @@ typedef enum DisplayType
     DT_CURSES,
     DT_SDL,
     DT_GTK,
+    DT_FBDEV,
     DT_NOGRAPHIC,
     DT_NONE,
 } DisplayType;
diff --git a/include/ui/console.h b/include/ui/console.h
index 4307b5f..eb65739 100644
--- a/include/ui/console.h
+++ b/include/ui/console.h
@@ -308,6 +308,10 @@ void register_vc_handler(VcHandler *handler);
 /* sdl.c */
 void sdl_display_init(DisplayState *ds, int full_screen, int no_frame);
 
+/* fbdev.c */
+int fbdev_display_init(const char *device, bool scale, Error **err);
+void fbdev_display_uninit(void);
+
 /* cocoa.m */
 void cocoa_display_init(DisplayState *ds, int full_screen);
 
diff --git a/qemu-options.hx b/qemu-options.hx
index bf94862..8e02863 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -804,7 +804,7 @@ ETEXI
 
 DEF("display", HAS_ARG, QEMU_OPTION_display,
     "-display sdl[,frame=on|off][,alt_grab=on|off][,ctrl_grab=on|off]\n"
-    "            [,window_close=on|off]|curses|none|\n"
+    "            [,window_close=on|off]|curses|fbdev|none|\n"
     "            vnc=<display>[,<optargs>]\n"
     "                select display type\n", QEMU_ARCH_ALL)
 STEXI
@@ -822,6 +822,8 @@ support a text mode, QEMU can display this output using a
 curses/ncurses interface. Nothing is displayed when the graphics
 device is in graphical mode or if the graphics device does not support
 a text mode. Generally only the VGA device models support text mode.
+@item fbdev
+Display video output on a linux framebuffer console.
 @item none
 Do not display video output. The guest will still see an emulated
 graphics card, but its output will not be displayed to the QEMU
diff --git a/trace-events b/trace-events
index c5f1ccb..7a73b2a 100644
--- a/trace-events
+++ b/trace-events
@@ -1161,3 +1161,17 @@ kvm_run_exit(int cpu_index, uint32_t reason) "cpu_index %d, reason %d"
 # qom/object.c
 object_dynamic_cast_assert(const char *type, const char *target, const char *file, int line, const char *func) "%s->%s (%s:%d:%s)"
 object_class_dynamic_cast_assert(const char *type, const char *target, const char *file, int line, const char *func) "%s->%s (%s:%d:%s)"
+
+# ui/fbdev.c
+fbdev_enabled(void) ""
+fbdev_cleanup(void) ""
+fbdev_vt_activate(int vtno, int wait) "vtno %d, wait %d"
+fbdev_vt_activated(void) ""
+fbdev_vt_release_request(void) ""
+fbdev_vt_released(void) ""
+fbdev_vt_aquire_request(void) ""
+fbdev_vt_aquired(void) ""
+fbdev_kbd_raw(int enable) "enable %d"
+fbdev_kbd_event(int keycode, const char *kname, int up) "keycode 0x%x [%s], down %d"
+fbdev_dpy_resize(int w, int h) "%dx%d"
+fbdev_dpy_redraw(void)
diff --git a/ui/Makefile.objs b/ui/Makefile.objs
index 6ddc0de..ee96ad5 100644
--- a/ui/Makefile.objs
+++ b/ui/Makefile.objs
@@ -14,6 +14,7 @@ common-obj-$(CONFIG_COCOA) += cocoa.o
 common-obj-$(CONFIG_CURSES) += curses.o
 common-obj-$(CONFIG_VNC) += $(vnc-obj-y)
 common-obj-$(CONFIG_GTK) += gtk.o x_keymap.o
+common-obj-$(CONFIG_FBDEV) += fbdev.o
 
 $(obj)/sdl.o $(obj)/sdl_zoom.o: QEMU_CFLAGS += $(SDL_CFLAGS) 
 
diff --git a/ui/fbdev.c b/ui/fbdev.c
new file mode 100644
index 0000000..4fe34d6
--- /dev/null
+++ b/ui/fbdev.c
@@ -0,0 +1,1123 @@
+/*
+ * linux fbdev output driver.
+ *
+ * Author: Gerd Hoffmann <kraxel@redhat.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 or later.
+ * See the COPYING file in the top-level directory.
+ *
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <termios.h>
+
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+
+#include <linux/kd.h>
+#include <linux/vt.h>
+#include <linux/fb.h>
+
+#include "qemu-common.h"
+#include "keymaps.h"
+#include "ui/qemu-pixman.h"
+#include "ui/console.h"
+#include "sysemu/sysemu.h"
+
+/*
+ * must be last so we get the linux input layer
+ * KEY_* defines, not the ncurses ones.
+ */
+#include <linux/input.h>
+
+/* -------------------------------------------------------------------- */
+
+/* file handles */
+static int                        tty = -1, fb = -1, mice = -1;
+
+/* saved state, for restore on exit */
+static int                        orig_vtno;
+static int                        kd_omode;
+static struct vt_mode             vt_omode;
+static struct fb_var_screeninfo   fb_ovar;
+
+/* framebuffer */
+static struct fb_fix_screeninfo   fb_fix;
+static struct fb_var_screeninfo   fb_var;
+static uint8_t                   *fb_mem;
+static int                        fb_mem_offset;
+
+/* linux console */
+static int                        vtno;
+static struct vt_mode             vt_mode;
+static struct termios             tty_attributes;
+static unsigned long              tty_mode;
+static unsigned int               tty_flags;
+static bool                       tty_mediumraw;
+static bool                       key_down[KEY_CNT];
+
+/* console switching */
+#define SIG_ACQ      (SIGRTMIN+6)
+#define SIG_REL      (SIGRTMIN+7)
+#define FB_ACTIVE    0
+#define FB_REL_REQ   1
+#define FB_INACTIVE  2
+#define FB_ACQ_REQ   3
+static int fb_switch_state;
+
+/* qemu windup */
+static DisplayChangeListener      *dcl;
+static int                        resize_screen;
+static int                        redraw_screen;
+static int                        cx, cy, cw, ch;
+static Notifier                   exit_notifier;
+static DisplaySurface             *surface;
+static pixman_image_t             *sref, *swork;
+static pixman_image_t             *framebuffer;
+static pixman_transform_t         transform;
+static pixman_region16_t          dirty;
+static double                     scale;
+
+static QEMUCursor                 *ptr_cursor;
+static pixman_image_t             *ptr_image;
+static int                        ptr_refresh;
+static int                        px, py, pw, ph;
+static int                        mx, my, mon;
+
+/* options */
+static int                        use_scale;
+static pixman_filter_t            pfilter = PIXMAN_FILTER_GOOD;
+
+/* fwd decls */
+static int fbdev_activate_vt(int tty, int vtno, bool wait);
+
+/* -------------------------------------------------------------------- */
+/* pixman helpers                                                       */
+
+static pixman_image_t *pixman_from_framebuffer(void)
+{
+    pixman_format_code_t format;
+    pixman_image_t *image;
+    int type;
+
+    type = qemu_pixman_get_type(fb_var.red.offset,
+                                fb_var.green.offset,
+                                fb_var.blue.offset);
+    format = PIXMAN_FORMAT(fb_var.bits_per_pixel, type,
+                           fb_var.transp.length,
+                           fb_var.red.length,
+                           fb_var.green.length,
+                           fb_var.blue.length);
+    image = pixman_image_create_bits(format, fb_var.xres, fb_var.yres,
+                                     (void *)fb_mem, fb_fix.line_length);
+    return image;
+}
+
+static pixman_image_t *pixman_image_clone(pixman_image_t *i)
+{
+    return pixman_image_create_bits(pixman_image_get_format(i),
+                                    pixman_image_get_width(i),
+                                    pixman_image_get_height(i),
+                                    pixman_image_get_data(i),
+                                    pixman_image_get_stride(i));
+}
+
+/* -------------------------------------------------------------------- */
+/* mouse                                                                */
+
+static void read_mouse(void *opaque)
+{
+    char buf[3];
+    int rc, x, y, b;
+
+    rc = read(mice, buf, sizeof(buf));
+    if (rc != sizeof(buf)) {
+        return;
+    }
+
+    if (fb_switch_state != FB_ACTIVE) {
+        return;
+    }
+
+    x = buf[1];
+    y = -buf[2];
+    b = buf[0] & 0x7;
+
+    if (kbd_mouse_is_absolute()) {
+        static int ax, ay;
+        ax += x; ay += y;
+        if (ax < 0) {
+            ax = 0;
+        }
+        if (ay < 0) {
+            ay = 0;
+        }
+        if (ax >= cw*scale) {
+            ax = cw*scale-1;
+        }
+        if (ay >= ch*scale) {
+            ay = ch*scale-1;
+        }
+        kbd_mouse_event(ax * 0x7FFF / (cw*scale),
+                        ay * 0x7FFF / (ch*scale), 0, b);
+    } else {
+        kbd_mouse_event(x, y, 0, b);
+    }
+}
+
+static int init_mouse(void)
+{
+    mice = open("/dev/input/mice", O_RDONLY);
+    if (mice == -1) {
+        return -1;
+    }
+    qemu_set_fd_handler(mice, read_mouse, NULL, NULL);
+    return 0;
+}
+
+static void uninit_mouse(void)
+{
+    if (mice == -1) {
+        return;
+    }
+    qemu_set_fd_handler(mice, NULL, NULL, NULL);
+    close(mice);
+    mice = -1;
+}
+
+/* -------------------------------------------------------------------- */
+/* keyboard                                                             */
+
+static const char *keynames[] = {
+#include "linux-keynames.h"
+};
+
+static const int scancode_map[KEY_CNT] = {
+    [KEY_ESC]           = 0x01,
+    [KEY_1]             = 0x02,
+    [KEY_2]             = 0x03,
+    [KEY_3]             = 0x04,
+    [KEY_4]             = 0x05,
+    [KEY_5]             = 0x06,
+    [KEY_6]             = 0x07,
+    [KEY_7]             = 0x08,
+    [KEY_8]             = 0x09,
+    [KEY_9]             = 0x0a,
+    [KEY_0]             = 0x0b,
+    [KEY_MINUS]         = 0x0c,
+    [KEY_EQUAL]         = 0x0d,
+    [KEY_BACKSPACE]     = 0x0e,
+
+    [KEY_TAB]           = 0x0f,
+    [KEY_Q]             = 0x10,
+    [KEY_W]             = 0x11,
+    [KEY_E]             = 0x12,
+    [KEY_R]             = 0x13,
+    [KEY_T]             = 0x14,
+    [KEY_Y]             = 0x15,
+    [KEY_U]             = 0x16,
+    [KEY_I]             = 0x17,
+    [KEY_O]             = 0x18,
+    [KEY_P]             = 0x19,
+    [KEY_LEFTBRACE]     = 0x1a,
+    [KEY_RIGHTBRACE]    = 0x1b,
+    [KEY_ENTER]         = 0x1c,
+
+    [KEY_A]             = 0x1e,
+    [KEY_S]             = 0x1f,
+    [KEY_D]             = 0x20,
+    [KEY_F]             = 0x21,
+    [KEY_G]             = 0x22,
+    [KEY_H]             = 0x23,
+    [KEY_J]             = 0x24,
+    [KEY_K]             = 0x25,
+    [KEY_L]             = 0x26,
+    [KEY_SEMICOLON]     = 0x27,
+    [KEY_APOSTROPHE]    = 0x28,
+    [KEY_GRAVE]         = 0x29,
+    [KEY_LEFTSHIFT]     = 0x2a,
+    [KEY_BACKSLASH]     = 0x2b,
+
+    [KEY_Z]             = 0x2c,
+    [KEY_X]             = 0x2d,
+    [KEY_C]             = 0x2e,
+    [KEY_V]             = 0x2f,
+    [KEY_B]             = 0x30,
+    [KEY_N]             = 0x31,
+    [KEY_M]             = 0x32,
+    [KEY_COMMA]         = 0x33,
+    [KEY_DOT]           = 0x34,
+    [KEY_SLASH]         = 0x35,
+    [KEY_RIGHTSHIFT]    = 0x36,
+    [KEY_SPACE]         = 0x39,
+
+    [KEY_F1]            = 0x3b,
+    [KEY_F2]            = 0x3c,
+    [KEY_F3]            = 0x3d,
+    [KEY_F4]            = 0x3e,
+    [KEY_F5]            = 0x3f,
+    [KEY_F6]            = 0x40,
+    [KEY_F7]            = 0x41,
+    [KEY_F8]            = 0x42,
+    [KEY_F9]            = 0x43,
+    [KEY_F10]           = 0x44,
+    [KEY_F11]           = 0x57,
+    [KEY_F12]           = 0x58,
+
+    [KEY_SYSRQ]         = 0xb7,
+    [KEY_SCROLLLOCK]    = 0x46,
+#if 0
+    [KEY_PAUSE]         = FIXME,
+#endif
+    [KEY_CAPSLOCK]      = 0x3a,
+    [KEY_102ND]         = 0x56,
+
+    [KEY_LEFTCTRL]      = 0x1d,
+    [KEY_LEFTMETA]      = 0xdb,
+    [KEY_LEFTALT]       = 0x38,
+    [KEY_RIGHTALT]      = 0xb8,
+    [KEY_RIGHTMETA]     = 0xdc,
+    [KEY_RIGHTCTRL]     = 0x9d,
+    [KEY_COMPOSE]       = 0xdd,
+
+    [KEY_INSERT]        = 0xd2,
+    [KEY_DELETE]        = 0xd3,
+    [KEY_HOME]          = 0xc7,
+    [KEY_END]           = 0xcf,
+    [KEY_PAGEUP]        = 0xc9,
+    [KEY_PAGEDOWN]      = 0xd1,
+
+    [KEY_UP]            = 0xc8,
+    [KEY_LEFT]          = 0xcb,
+    [KEY_RIGHT]         = 0xcd,
+    [KEY_DOWN]          = 0xd0,
+
+    [KEY_NUMLOCK]       = 0x45,
+    [KEY_KPSLASH]       = 0xb5,
+    [KEY_KPASTERISK]    = 0x37,
+    [KEY_KP7]           = 0x47,
+    [KEY_KP8]           = 0x48,
+    [KEY_KP9]           = 0x49,
+    [KEY_KPMINUS]       = 0x4a,
+    [KEY_KP4]           = 0x4b,
+    [KEY_KP5]           = 0x4c,
+    [KEY_KP6]           = 0x4d,
+    [KEY_KPPLUS]        = 0x4e,
+    [KEY_KP1]           = 0x4f,
+    [KEY_KP2]           = 0x50,
+    [KEY_KP3]           = 0x51,
+    [KEY_KP0]           = 0x52,
+    [KEY_KPDOT]         = 0x53,
+    [KEY_KPENTER]       = 0x9c,
+};
+
+static const struct keysym_map {
+    int  normal, shifted;
+} keysym_map_en_us[KEY_CNT] = {
+    [KEY_A] = { .normal = 'a', .shifted = 'A' },
+    [KEY_B] = { .normal = 'b', .shifted = 'B' },
+    [KEY_C] = { .normal = 'c', .shifted = 'C' },
+    [KEY_D] = { .normal = 'd', .shifted = 'D' },
+    [KEY_E] = { .normal = 'e', .shifted = 'E' },
+    [KEY_F] = { .normal = 'f', .shifted = 'F' },
+    [KEY_G] = { .normal = 'g', .shifted = 'G' },
+    [KEY_H] = { .normal = 'h', .shifted = 'H' },
+    [KEY_I] = { .normal = 'i', .shifted = 'I' },
+    [KEY_J] = { .normal = 'j', .shifted = 'J' },
+    [KEY_K] = { .normal = 'k', .shifted = 'K' },
+    [KEY_L] = { .normal = 'l', .shifted = 'L' },
+    [KEY_M] = { .normal = 'm', .shifted = 'M' },
+    [KEY_N] = { .normal = 'n', .shifted = 'N' },
+    [KEY_O] = { .normal = 'o', .shifted = 'O' },
+    [KEY_P] = { .normal = 'p', .shifted = 'P' },
+    [KEY_Q] = { .normal = 'q', .shifted = 'Q' },
+    [KEY_R] = { .normal = 'r', .shifted = 'R' },
+    [KEY_S] = { .normal = 's', .shifted = 'S' },
+    [KEY_T] = { .normal = 't', .shifted = 'T' },
+    [KEY_U] = { .normal = 'u', .shifted = 'U' },
+    [KEY_V] = { .normal = 'v', .shifted = 'V' },
+    [KEY_W] = { .normal = 'w', .shifted = 'W' },
+    [KEY_X] = { .normal = 'x', .shifted = 'X' },
+    [KEY_Y] = { .normal = 'y', .shifted = 'Y' },
+    [KEY_Z] = { .normal = 'z', .shifted = 'Z' },
+
+    [KEY_1] = { .normal = '1', .shifted = '!' },
+    [KEY_2] = { .normal = '2', .shifted = '@' },
+    [KEY_3] = { .normal = '3', .shifted = '#' },
+    [KEY_4] = { .normal = '4', .shifted = '$' },
+    [KEY_5] = { .normal = '5', .shifted = '%' },
+    [KEY_6] = { .normal = '6', .shifted = '^' },
+    [KEY_7] = { .normal = '7', .shifted = '&' },
+    [KEY_8] = { .normal = '8', .shifted = '*' },
+    [KEY_9] = { .normal = '9', .shifted = '(' },
+    [KEY_0] = { .normal = '0', .shifted = ')' },
+
+    [KEY_MINUS]       = { .normal = '-',  .shifted = '_'  },
+    [KEY_EQUAL]       = { .normal = '=',  .shifted = '+'  },
+    [KEY_TAB]         = { .normal = '\t'  },
+    [KEY_LEFTBRACE]   = { .normal = '[',  .shifted = '{'  },
+    [KEY_RIGHTBRACE]  = { .normal = ']',  .shifted = '}'  },
+    [KEY_ENTER]       = { .normal = '\n', },
+    [KEY_SEMICOLON]   = { .normal = ';',  .shifted = ':'  },
+    [KEY_APOSTROPHE]  = { .normal = '"',  .shifted = '\'' },
+    [KEY_BACKSLASH]   = { .normal = '\\', .shifted = '|'  },
+    [KEY_COMMA]       = { .normal = ',',  .shifted = '<'  },
+    [KEY_DOT]         = { .normal = '.',  .shifted = '>'  },
+    [KEY_SLASH]       = { .normal = '/',  .shifted = '?'  },
+    [KEY_SPACE]       = { .normal = ' '   },
+
+    [KEY_BACKSPACE]   = { .normal = QEMU_KEY_BACKSPACE  },
+    [KEY_UP]          = { .normal = QEMU_KEY_UP         },
+    [KEY_DOWN]        = { .normal = QEMU_KEY_DOWN       },
+    [KEY_LEFT]        = { .normal = QEMU_KEY_LEFT       },
+    [KEY_RIGHT]       = { .normal = QEMU_KEY_RIGHT      },
+};
+
+static void start_mediumraw(int tty)
+{
+    struct termios tattr;
+
+    if (tty_mediumraw) {
+        return;
+    }
+    trace_fbdev_kbd_raw(1);
+
+    /* save state */
+    tcgetattr(tty, &tty_attributes);
+    ioctl(tty, KDGKBMODE, &tty_mode);
+    tty_flags = fcntl(tty, F_GETFL, NULL);
+
+    /* setup */
+    tattr = tty_attributes;
+    tattr.c_cflag &= ~(IXON|IXOFF);
+    tattr.c_lflag &= ~(ICANON|ECHO|ISIG);
+    tattr.c_iflag = 0;
+    tattr.c_cc[VMIN] = 1;
+    tattr.c_cc[VTIME] = 0;
+    tcsetattr(tty, TCSAFLUSH, &tattr);
+    ioctl(tty, KDSKBMODE, K_MEDIUMRAW);
+    fcntl(tty, F_SETFL, tty_flags | O_NONBLOCK);
+
+    tty_mediumraw = true;
+}
+
+static void stop_mediumraw(int tty)
+{
+    if (!tty_mediumraw) {
+        return;
+    }
+    trace_fbdev_kbd_raw(0);
+
+    /* restore state */
+    tcsetattr(tty, TCSANOW, &tty_attributes);
+    ioctl(tty, KDSKBMODE, tty_mode);
+    fcntl(tty, F_SETFL, tty_flags);
+
+    tty_mediumraw = false;
+}
+
+static void send_scancode(int keycode, int up)
+{
+    int scancode = scancode_map[keycode];
+
+    if (!scancode) {
+        fprintf(stderr, "%s: unmapped key: 0x%x %s\n",
+                __func__, keycode, keynames[keycode]);
+        return;
+    }
+    if (scancode & SCANCODE_GREY) {
+        kbd_put_keycode(SCANCODE_EMUL0);
+    }
+    if (up) {
+        kbd_put_keycode(scancode | SCANCODE_UP);
+    } else {
+        kbd_put_keycode(scancode & SCANCODE_KEYCODEMASK);
+    }
+}
+
+static void send_keysym(int keycode, int shift)
+{
+    const struct keysym_map *keysym_map = keysym_map_en_us;
+    int keysym;
+
+    if (shift && keysym_map[keycode].shifted) {
+        keysym = keysym_map[keycode].shifted;
+    } else if (keysym_map[keycode].normal) {
+        keysym = keysym_map[keycode].normal;
+    } else {
+        fprintf(stderr, "%s: unmapped key: 0x%x %s\n",
+                __func__, keycode, keynames[keycode]);
+        return;
+    }
+    kbd_put_keysym(keysym);
+}
+
+static void reset_keys(void)
+{
+    int keycode;
+
+    for (keycode = 0; keycode < KEY_MAX; keycode++) {
+        if (key_down[keycode]) {
+            if (qemu_console_is_graphic(NULL)) {
+                send_scancode(keycode, 1);
+            }
+            key_down[keycode] = false;
+        }
+    }
+}
+
+static void read_mediumraw(void *opaque)
+{
+    uint8_t buf[32];
+    int i, rc, up, keycode;
+    bool ctrl, alt, shift;
+
+    rc = read(tty, buf, sizeof(buf));
+    switch (rc) {
+    case -1:
+        perror("read tty");
+        goto err;
+    case 0:
+        fprintf(stderr, "%s: eof\n", __func__);
+        goto err;
+    default:
+        for (i = 0; i < rc; i++) {
+            up      = buf[i] & 0x80;
+            keycode = buf[i] & 0x7f;
+            if (keycode == 0) {
+                keycode  = (buf[i+1] & 0x7f) << 7;
+                keycode |= buf[i+2] & 0x7f;
+                i += 2;
+            }
+            if (keycode > KEY_MAX) {
+                continue;
+            }
+
+            if (up) {
+                if (!key_down[keycode]) {
+                    continue;
+                }
+                key_down[keycode] = false;
+            } else {
+                key_down[keycode] = true;
+            }
+
+            trace_fbdev_kbd_event(keycode, keynames[keycode], !up);
+
+            alt   = key_down[KEY_LEFTALT]   || key_down[KEY_RIGHTALT];
+            ctrl  = key_down[KEY_LEFTCTRL]  || key_down[KEY_RIGHTCTRL];
+            shift = key_down[KEY_LEFTSHIFT] || key_down[KEY_RIGHTSHIFT];
+
+            if (ctrl && alt && !up) {
+                if (keycode == KEY_ESC) {
+                    fprintf(stderr, "=== fbdev emergency escape "
+                            "(ctrl-alt-esc) ===\n");
+                    exit(1);
+                }
+                if (keycode == KEY_S) {
+                    use_scale = !use_scale;
+                    resize_screen++;
+                    redraw_screen++;
+                    continue;
+                }
+                if (keycode >= KEY_F1 && keycode <= KEY_F10) {
+                    fbdev_activate_vt(tty, keycode+1-KEY_F1, false);
+                    key_down[keycode] = false;
+                    continue;
+                }
+                if (keycode >= KEY_1 && keycode <= KEY_9) {
+                    console_select(keycode-KEY_1);
+                    reset_keys();
+                    continue;
+                }
+            }
+
+            if (qemu_console_is_graphic(NULL)) {
+                send_scancode(keycode, up);
+            } else if (!up) {
+                send_keysym(keycode, shift);
+            }
+        }
+    }
+    return;
+
+err:
+    exit(1);
+}
+
+/* -------------------------------------------------------------------- */
+
+static void fbdev_cls(void)
+{
+    memset(fb_mem + fb_mem_offset, 0, fb_fix.line_length * fb_var.yres);
+}
+
+static int fbdev_activate_vt(int tty, int vtno, bool wait)
+{
+    trace_fbdev_vt_activate(vtno, wait);
+
+    if (ioctl(tty, VT_ACTIVATE, vtno) < 0) {
+        perror("ioctl VT_ACTIVATE");
+        return -1;
+    }
+
+    if (wait) {
+        if (ioctl(tty, VT_WAITACTIVE, vtno) < 0) {
+            perror("ioctl VT_WAITACTIVE");
+            return -1;
+        }
+        trace_fbdev_vt_activated();
+    }
+
+    return 0;
+}
+
+static void fbdev_cleanup(void)
+{
+    trace_fbdev_cleanup();
+
+    /* release pixman stuff */
+    pixman_region_fini(&dirty);
+    if (framebuffer) {
+        pixman_image_unref(framebuffer);
+        framebuffer = NULL;
+    }
+    if (sref) {
+        pixman_image_unref(sref);
+        sref = NULL;
+    }
+    if (swork) {
+        pixman_image_unref(swork);
+        swork = NULL;
+    }
+
+    /* restore console */
+    if (fb_mem != NULL) {
+        munmap(fb_mem, fb_fix.smem_len+fb_mem_offset);
+        fb_mem = NULL;
+    }
+    if (fb != -1) {
+        if (ioctl(fb, FBIOPUT_VSCREENINFO, &fb_ovar) < 0) {
+            perror("ioctl FBIOPUT_VSCREENINFO");
+        }
+        close(fb);
+        fb = -1;
+    }
+
+    if (tty != -1) {
+        stop_mediumraw(tty);
+        if (ioctl(tty, KDSETMODE, kd_omode) < 0) {
+            perror("ioctl KDSETMODE");
+        }
+        if (ioctl(tty, VT_SETMODE, &vt_omode) < 0) {
+            perror("ioctl VT_SETMODE");
+        }
+        if (orig_vtno) {
+            fbdev_activate_vt(tty, orig_vtno, true);
+        }
+        qemu_set_fd_handler(tty, NULL, NULL, NULL);
+        close(tty);
+        tty = -1;
+    }
+}
+
+static int fbdev_init(const char *device, Error **err)
+{
+    struct vt_stat vts;
+    unsigned long page_mask;
+    char ttyname[32];
+
+    /* open framebuffer */
+    if (device == NULL) {
+        device = getenv("FRAMEBUFFER");
+    }
+    if (device == NULL) {
+        device = "/dev/fb0";
+    }
+    fb = open(device, O_RDWR);
+    if (fb == -1) {
+        error_setg(err, "open %s: %s\n", device, strerror(errno));
+        return -1;
+    }
+
+    /* open virtual console */
+    tty = 0;
+    if (ioctl(tty, VT_GETSTATE, &vts) < 0) {
+        fprintf(stderr, "Not started from virtual terminal, "
+                "trying to open one.\n");
+
+        snprintf(ttyname, sizeof(ttyname), "/dev/tty0");
+        tty = open(ttyname, O_RDWR);
+        if (tty == -1) {
+            error_setg(err, "open %s: %s\n", ttyname, strerror(errno));
+            goto err_early;
+        }
+        if (ioctl(tty, VT_OPENQRY, &vtno) < 0) {
+            error_setg(err, "ioctl VT_OPENQRY: %s\n", strerror(errno));
+            goto err_early;
+        }
+        if (ioctl(tty, VT_GETSTATE, &vts) < 0) {
+            error_setg(err, "ioctl VT_GETSTATE: %s\n", strerror(errno));
+            goto err_early;
+        }
+        close(tty);
+
+        snprintf(ttyname, sizeof(ttyname), "/dev/tty%d", vtno);
+        tty = open(ttyname, O_RDWR);
+        if (tty == -1) {
+            error_setg(err, "open %s: %s\n", ttyname, strerror(errno));
+            goto err_early;
+        }
+        orig_vtno = vts.v_active;
+        fprintf(stderr, "Switching to vt %d (current %d).\n", vtno, orig_vtno);
+    } else {
+        orig_vtno = 0;
+        vtno = vts.v_active;
+        fprintf(stderr, "Started at vt %d, using it.\n", vtno);
+    }
+    fbdev_activate_vt(tty, vtno, true);
+
+    /* get current settings (which we have to restore) */
+    if (ioctl(fb, FBIOGET_VSCREENINFO, &fb_ovar) < 0) {
+        error_setg(err, "ioctl FBIOGET_VSCREENINFO: %s\n", strerror(errno));
+        goto err_early;
+    }
+    if (ioctl(tty, KDGETMODE, &kd_omode) < 0) {
+        error_setg(err, "ioctl KDGETMODE: %s\n", strerror(errno));
+        goto err_early;
+    }
+    if (ioctl(tty, VT_GETMODE, &vt_omode) < 0) {
+        error_setg(err, "ioctl VT_GETMODE: %s\n", strerror(errno));
+        goto err_early;
+    }
+
+    /* checks & initialisation */
+    if (ioctl(fb, FBIOGET_FSCREENINFO, &fb_fix) < 0) {
+        error_setg(err, "ioctl : %s\n", strerror(errno));
+        perror("ioctl FBIOGET_FSCREENINFO");
+        goto err;
+    }
+    if (ioctl(fb, FBIOGET_VSCREENINFO, &fb_var) < 0) {
+        error_setg(err, "ioctl FBIOGET_VSCREENINFO: %s\n", strerror(errno));
+        goto err;
+    }
+    if (fb_fix.type != FB_TYPE_PACKED_PIXELS) {
+        error_setg(err, "can handle only packed pixel frame buffers\n");
+        goto err;
+    }
+    switch (fb_var.bits_per_pixel) {
+    case 32:
+        break;
+    default:
+        error_setg(err, "can't handle %d bpp frame buffers\n",
+                fb_var.bits_per_pixel);
+        goto err;
+    }
+
+    page_mask = getpagesize()-1;
+    fb_switch_state = FB_ACTIVE;
+    fb_mem_offset = (unsigned long)(fb_fix.smem_start) & page_mask;
+    fb_mem = mmap(NULL, fb_fix.smem_len+fb_mem_offset,
+                  PROT_READ|PROT_WRITE, MAP_SHARED, fb, 0);
+    if (fb_mem == MAP_FAILED) {
+        error_setg(err, "mmap: %s\n", strerror(errno));
+        goto err;
+    }
+    /* move viewport to upper left corner */
+    if (fb_var.xoffset != 0 || fb_var.yoffset != 0) {
+        fb_var.xoffset = 0;
+        fb_var.yoffset = 0;
+        if (ioctl(fb, FBIOPAN_DISPLAY, &fb_var) < 0) {
+            error_setg(err, "ioctl FBIOPAN_DISPLAY: %s\n", strerror(errno));
+            goto err;
+        }
+    }
+    if (ioctl(tty, KDSETMODE, KD_GRAPHICS) < 0) {
+        error_setg(err, "ioctl KDSETMODE: %s\n", strerror(errno));
+        goto err;
+    }
+    /* some fb drivers need this again after switching to graphics ... */
+    fbdev_activate_vt(tty, vtno, true);
+
+    fbdev_cls();
+
+    start_mediumraw(tty);
+    qemu_set_fd_handler(tty, read_mediumraw, NULL, NULL);
+
+    framebuffer = pixman_from_framebuffer();
+    pixman_region_init(&dirty);
+    return 0;
+
+err_early:
+    if (tty > 0) {
+        close(tty);
+    }
+    close(fb);
+    return -1;
+
+err:
+    fbdev_cleanup();
+    return -1;
+}
+
+static void
+fbdev_catch_fatal_signal(int signr)
+{
+    fprintf(stderr, "%s: %s, restoring linux console state ...\n",
+            __func__, strsignal(signr));
+    fbdev_cleanup();
+    signal(SIGABRT, SIG_DFL);
+    fprintf(stderr, "%s: ... done, going abort() now.\n", __func__);
+    abort();
+}
+
+static void fbdev_catch_exit_signals(void)
+{
+    static const int signals[] = {
+        SIGQUIT, SIGILL, SIGABRT, SIGFPE, SIGSEGV, SIGBUS
+    };
+    struct sigaction act, old;
+    int i;
+
+    memset(&act, 0, sizeof(act));
+    act.sa_handler = fbdev_catch_fatal_signal;
+    act.sa_flags = SA_RESETHAND;
+    sigemptyset(&act.sa_mask);
+    for (i = 0; i < ARRAY_SIZE(signals); i++) {
+        sigaction(signals[i], &act, &old);
+    }
+}
+
+/* -------------------------------------------------------------------- */
+/* console switching                                                    */
+
+static void fbdev_switch_signal(int signal)
+{
+    if (signal == SIG_REL) {
+        /* release */
+        trace_fbdev_vt_release_request();
+        fb_switch_state = FB_REL_REQ;
+    }
+    if (signal == SIG_ACQ) {
+        /* acquisition */
+        trace_fbdev_vt_aquire_request();
+        fb_switch_state = FB_ACQ_REQ;
+    }
+}
+
+static void fbdev_switch_release(void)
+{
+    stop_mediumraw(tty);
+    ioctl(tty, KDSETMODE, kd_omode);
+    ioctl(tty, VT_RELDISP, 1);
+    fb_switch_state = FB_INACTIVE;
+    trace_fbdev_vt_released();
+}
+
+static void fbdev_switch_acquire(void)
+{
+    ioctl(tty, VT_RELDISP, VT_ACKACQ);
+    start_mediumraw(tty);
+    reset_keys();
+    ioctl(tty, KDSETMODE, KD_GRAPHICS);
+    fb_switch_state = FB_ACTIVE;
+    trace_fbdev_vt_aquired();
+}
+
+static int fbdev_switch_init(void)
+{
+    struct sigaction act, old;
+
+    memset(&act, 0, sizeof(act));
+    act.sa_handler  = fbdev_switch_signal;
+    sigemptyset(&act.sa_mask);
+    sigaction(SIG_REL, &act, &old);
+    sigaction(SIG_ACQ, &act, &old);
+
+    if (ioctl(tty, VT_GETMODE, &vt_mode) < 0) {
+        perror("ioctl VT_GETMODE");
+        exit(1);
+    }
+    vt_mode.mode   = VT_PROCESS;
+    vt_mode.waitv  = 0;
+    vt_mode.relsig = SIG_REL;
+    vt_mode.acqsig = SIG_ACQ;
+
+    if (ioctl(tty, VT_SETMODE, &vt_mode) < 0) {
+        perror("ioctl VT_SETMODE");
+        exit(1);
+    }
+    return 0;
+}
+
+/* -------------------------------------------------------------------- */
+/* rendering                                                            */
+
+static void fbdev_render(void)
+{
+    assert(surface);
+
+    pixman_image_set_clip_region(swork, &dirty);
+    pixman_image_composite(PIXMAN_OP_SRC, swork, NULL, framebuffer,
+                           0, 0, 0, 0, 0, 0, fb_var.xres, fb_var.yres);
+    pixman_region_fini(&dirty);
+    pixman_region_init(&dirty);
+}
+
+static void fbdev_unrender_ptr(void)
+{
+    if (!pw && !ph) {
+        return;
+    }
+    pixman_region_union_rect(&dirty, &dirty, px, py, pw, ph);
+    ph = pw = 0;
+}
+
+static void fbdev_render_ptr(void)
+{
+    pixman_region16_t region;
+    pixman_transform_t transform;
+
+    if (!mon || !ptr_image) {
+        return;
+    }
+    if (mx < 0 || mx >= cw || my < 0 || my >= ch) {
+        return;
+    }
+
+    px = mx - ptr_cursor->hot_x;
+    py = my - ptr_cursor->hot_y;
+    pw = ptr_cursor->width;
+    ph = ptr_cursor->height;
+
+    pixman_transform_init_identity(&transform);
+    pixman_transform_translate(&transform, NULL,
+                               pixman_int_to_fixed(-cx),
+                               pixman_int_to_fixed(-cy));
+    if (use_scale) {
+        pixman_transform_scale(&transform, NULL,
+                               pixman_double_to_fixed(1/scale),
+                               pixman_double_to_fixed(1/scale));
+    }
+    pixman_transform_translate(&transform, NULL,
+                               pixman_int_to_fixed(-px),
+                               pixman_int_to_fixed(-py));
+    pixman_image_set_transform(ptr_image, &transform);
+
+    pixman_region_init_rect(&region, 0, 0, pw, ph);
+    pixman_image_set_clip_region(ptr_image, &region);
+
+    pixman_image_composite(PIXMAN_OP_OVER, ptr_image, NULL, framebuffer,
+                           0, 0, 0, 0, 0, 0, fb_var.xres, fb_var.yres);
+
+    pixman_region_fini(&region);
+    ptr_refresh = 0;
+}
+
+/* -------------------------------------------------------------------- */
+/* qemu interfaces                                                      */
+
+static void fbdev_update(DisplayChangeListener *dcl,
+                         int x, int y, int w, int h)
+{
+    if (fb_switch_state != FB_ACTIVE) {
+        return;
+    }
+
+    if (resize_screen) {
+        double xs, ys;
+
+        trace_fbdev_dpy_resize(surface_width(surface),
+                               surface_height(surface));
+        resize_screen = 0;
+        cx = 0; cy = 0;
+        cw = surface_width(surface);
+        ch = surface_height(surface);
+
+        if (use_scale) {
+            xs = (double)fb_var.xres / cw;
+            ys = (double)fb_var.yres / ch;
+            if (xs > ys) {
+                scale = ys;
+                cx = (fb_var.xres - surface_width(surface)*scale) / 2;
+            } else {
+                scale = xs;
+                cy = (fb_var.yres - surface_height(surface)*scale) / 2;
+            }
+        } else {
+            scale = 1;
+            if (surface_width(surface) < fb_var.xres) {
+                cx = (fb_var.xres - surface_width(surface)) / 2;
+            }
+            if (surface_height(surface) < fb_var.yres) {
+                cy = (fb_var.yres - surface_height(surface)) / 2;
+            }
+        }
+        if (sref) {
+            pixman_image_unref(sref);
+        }
+        sref = pixman_image_ref(surface->image);
+
+        if (swork) {
+            pixman_image_unref(swork);
+        }
+        swork = pixman_image_clone(sref);
+
+        pixman_transform_init_identity(&transform);
+        pixman_transform_translate(&transform, NULL,
+                                   pixman_int_to_fixed(-cx),
+                                   pixman_int_to_fixed(-cy));
+        if (use_scale) {
+            pixman_transform_scale(&transform, NULL,
+                                   pixman_double_to_fixed(1/scale),
+                                   pixman_double_to_fixed(1/scale));
+        }
+        pixman_image_set_transform(swork, &transform);
+
+        pixman_image_set_filter(swork, pfilter, NULL, 0);
+    }
+
+    if (redraw_screen) {
+        trace_fbdev_dpy_redraw();
+        redraw_screen = 0;
+        fbdev_cls();
+        x = 0; y = 0; w = surface_width(surface); h = surface_height(surface);
+    }
+
+    pixman_region_union_rect(&dirty, &dirty, x, y, w, h);
+    if (ptr_image && mon && pw && ph) {
+        ptr_refresh++;
+    }
+}
+
+static void fbdev_switch(DisplayChangeListener *dcl,
+                         DisplaySurface *new_surface)
+{
+    surface = new_surface,
+    resize_screen++;
+    redraw_screen++;
+}
+
+static void fbdev_refresh(DisplayChangeListener *dcl)
+{
+    switch (fb_switch_state) {
+    case FB_REL_REQ:
+        fbdev_switch_release();
+        /* fall though */
+    case FB_INACTIVE:
+        return;
+    case FB_ACQ_REQ:
+        fbdev_switch_acquire();
+        redraw_screen++;
+        /* fall though */
+    case FB_ACTIVE:
+        break;
+    }
+
+    graphic_hw_update(NULL);
+    if (redraw_screen) {
+        fbdev_update(dcl, 0, 0, 0, 0);
+    }
+
+    if (ptr_refresh) {
+        fbdev_unrender_ptr();
+    }
+    if (pixman_region_not_empty(&dirty)) {
+        fbdev_render();
+    }
+    if (ptr_refresh) {
+        fbdev_render_ptr();
+    }
+}
+
+static void fbdev_mouse_set(DisplayChangeListener *dcl, int x, int y, int on)
+{
+    ptr_refresh++;
+    mx = x;
+    my = y;
+    mon = on;
+}
+
+static void fbdev_cursor_define(DisplayChangeListener *dcl, QEMUCursor *cursor)
+{
+    ptr_refresh++;
+
+    if (ptr_cursor) {
+        cursor_put(ptr_cursor);
+        ptr_cursor = NULL;
+    }
+    if (ptr_image) {
+        pixman_image_unref(ptr_image);
+        ptr_image = NULL;
+    }
+
+    if (!cursor) {
+        return;
+    }
+
+    ptr_cursor = cursor;
+    cursor_get(ptr_cursor);
+    ptr_image = pixman_image_create_bits(PIXMAN_a8r8g8b8,
+                                         cursor->width, cursor->height,
+                                         cursor->data,
+                                         cursor->width * 4);
+    pixman_image_set_filter(ptr_image, pfilter, NULL, 0);
+}
+
+static const DisplayChangeListenerOps fbdev_ops = {
+    .dpy_name          = "fbdev",
+    .dpy_gfx_update    = fbdev_update,
+    .dpy_gfx_switch    = fbdev_switch,
+    .dpy_refresh       = fbdev_refresh,
+    .dpy_mouse_set     = fbdev_mouse_set,
+    .dpy_cursor_define = fbdev_cursor_define,
+};
+
+static void fbdev_exit_notifier(Notifier *notifier, void *data)
+{
+    fbdev_cleanup();
+}
+
+int fbdev_display_init(const char *device, bool scale, Error **err)
+{
+    if (dcl != NULL) {
+        return 0;
+    }
+
+    if (fbdev_init(device, err) != 0) {
+        return -1;
+    }
+    exit_notifier.notify = fbdev_exit_notifier;
+    qemu_add_exit_notifier(&exit_notifier);
+    fbdev_switch_init();
+    fbdev_catch_exit_signals();
+    init_mouse();
+    use_scale = scale;
+
+    dcl = g_new0(DisplayChangeListener, 1);
+    dcl->ops = &fbdev_ops;
+    register_displaychangelistener(dcl);
+
+    trace_fbdev_enabled();
+    return 0;
+}
+
+void fbdev_display_uninit(void)
+{
+    if (dcl == NULL) {
+        return;
+    }
+
+    unregister_displaychangelistener(dcl);
+    g_free(dcl);
+    dcl = NULL;
+
+    fbdev_cleanup();
+    qemu_remove_exit_notifier(&exit_notifier);
+    uninit_mouse();
+}
diff --git a/ui/linux-keynames.h b/ui/linux-keynames.h
new file mode 100644
index 0000000..058af28
--- /dev/null
+++ b/ui/linux-keynames.h
@@ -0,0 +1,388 @@
+/*
+ *   awk '/#define KEY_/ { printf("    [%s] = \"%s\",\n",$2,$2); }' \
+ *       /usr/include/linux/input.h
+ */
+    [KEY_RESERVED] = "KEY_RESERVED",
+    [KEY_ESC] = "KEY_ESC",
+    [KEY_1] = "KEY_1",
+    [KEY_2] = "KEY_2",
+    [KEY_3] = "KEY_3",
+    [KEY_4] = "KEY_4",
+    [KEY_5] = "KEY_5",
+    [KEY_6] = "KEY_6",
+    [KEY_7] = "KEY_7",
+    [KEY_8] = "KEY_8",
+    [KEY_9] = "KEY_9",
+    [KEY_0] = "KEY_0",
+    [KEY_MINUS] = "KEY_MINUS",
+    [KEY_EQUAL] = "KEY_EQUAL",
+    [KEY_BACKSPACE] = "KEY_BACKSPACE",
+    [KEY_TAB] = "KEY_TAB",
+    [KEY_Q] = "KEY_Q",
+    [KEY_W] = "KEY_W",
+    [KEY_E] = "KEY_E",
+    [KEY_R] = "KEY_R",
+    [KEY_T] = "KEY_T",
+    [KEY_Y] = "KEY_Y",
+    [KEY_U] = "KEY_U",
+    [KEY_I] = "KEY_I",
+    [KEY_O] = "KEY_O",
+    [KEY_P] = "KEY_P",
+    [KEY_LEFTBRACE] = "KEY_LEFTBRACE",
+    [KEY_RIGHTBRACE] = "KEY_RIGHTBRACE",
+    [KEY_ENTER] = "KEY_ENTER",
+    [KEY_LEFTCTRL] = "KEY_LEFTCTRL",
+    [KEY_A] = "KEY_A",
+    [KEY_S] = "KEY_S",
+    [KEY_D] = "KEY_D",
+    [KEY_F] = "KEY_F",
+    [KEY_G] = "KEY_G",
+    [KEY_H] = "KEY_H",
+    [KEY_J] = "KEY_J",
+    [KEY_K] = "KEY_K",
+    [KEY_L] = "KEY_L",
+    [KEY_SEMICOLON] = "KEY_SEMICOLON",
+    [KEY_APOSTROPHE] = "KEY_APOSTROPHE",
+    [KEY_GRAVE] = "KEY_GRAVE",
+    [KEY_LEFTSHIFT] = "KEY_LEFTSHIFT",
+    [KEY_BACKSLASH] = "KEY_BACKSLASH",
+    [KEY_Z] = "KEY_Z",
+    [KEY_X] = "KEY_X",
+    [KEY_C] = "KEY_C",
+    [KEY_V] = "KEY_V",
+    [KEY_B] = "KEY_B",
+    [KEY_N] = "KEY_N",
+    [KEY_M] = "KEY_M",
+    [KEY_COMMA] = "KEY_COMMA",
+    [KEY_DOT] = "KEY_DOT",
+    [KEY_SLASH] = "KEY_SLASH",
+    [KEY_RIGHTSHIFT] = "KEY_RIGHTSHIFT",
+    [KEY_KPASTERISK] = "KEY_KPASTERISK",
+    [KEY_LEFTALT] = "KEY_LEFTALT",
+    [KEY_SPACE] = "KEY_SPACE",
+    [KEY_CAPSLOCK] = "KEY_CAPSLOCK",
+    [KEY_F1] = "KEY_F1",
+    [KEY_F2] = "KEY_F2",
+    [KEY_F3] = "KEY_F3",
+    [KEY_F4] = "KEY_F4",
+    [KEY_F5] = "KEY_F5",
+    [KEY_F6] = "KEY_F6",
+    [KEY_F7] = "KEY_F7",
+    [KEY_F8] = "KEY_F8",
+    [KEY_F9] = "KEY_F9",
+    [KEY_F10] = "KEY_F10",
+    [KEY_NUMLOCK] = "KEY_NUMLOCK",
+    [KEY_SCROLLLOCK] = "KEY_SCROLLLOCK",
+    [KEY_KP7] = "KEY_KP7",
+    [KEY_KP8] = "KEY_KP8",
+    [KEY_KP9] = "KEY_KP9",
+    [KEY_KPMINUS] = "KEY_KPMINUS",
+    [KEY_KP4] = "KEY_KP4",
+    [KEY_KP5] = "KEY_KP5",
+    [KEY_KP6] = "KEY_KP6",
+    [KEY_KPPLUS] = "KEY_KPPLUS",
+    [KEY_KP1] = "KEY_KP1",
+    [KEY_KP2] = "KEY_KP2",
+    [KEY_KP3] = "KEY_KP3",
+    [KEY_KP0] = "KEY_KP0",
+    [KEY_KPDOT] = "KEY_KPDOT",
+    [KEY_ZENKAKUHANKAKU] = "KEY_ZENKAKUHANKAKU",
+    [KEY_102ND] = "KEY_102ND",
+    [KEY_F11] = "KEY_F11",
+    [KEY_F12] = "KEY_F12",
+    [KEY_RO] = "KEY_RO",
+    [KEY_KATAKANA] = "KEY_KATAKANA",
+    [KEY_HIRAGANA] = "KEY_HIRAGANA",
+    [KEY_HENKAN] = "KEY_HENKAN",
+    [KEY_KATAKANAHIRAGANA] = "KEY_KATAKANAHIRAGANA",
+    [KEY_MUHENKAN] = "KEY_MUHENKAN",
+    [KEY_KPJPCOMMA] = "KEY_KPJPCOMMA",
+    [KEY_KPENTER] = "KEY_KPENTER",
+    [KEY_RIGHTCTRL] = "KEY_RIGHTCTRL",
+    [KEY_KPSLASH] = "KEY_KPSLASH",
+    [KEY_SYSRQ] = "KEY_SYSRQ",
+    [KEY_RIGHTALT] = "KEY_RIGHTALT",
+    [KEY_LINEFEED] = "KEY_LINEFEED",
+    [KEY_HOME] = "KEY_HOME",
+    [KEY_UP] = "KEY_UP",
+    [KEY_PAGEUP] = "KEY_PAGEUP",
+    [KEY_LEFT] = "KEY_LEFT",
+    [KEY_RIGHT] = "KEY_RIGHT",
+    [KEY_END] = "KEY_END",
+    [KEY_DOWN] = "KEY_DOWN",
+    [KEY_PAGEDOWN] = "KEY_PAGEDOWN",
+    [KEY_INSERT] = "KEY_INSERT",
+    [KEY_DELETE] = "KEY_DELETE",
+    [KEY_MACRO] = "KEY_MACRO",
+    [KEY_MUTE] = "KEY_MUTE",
+    [KEY_VOLUMEDOWN] = "KEY_VOLUMEDOWN",
+    [KEY_VOLUMEUP] = "KEY_VOLUMEUP",
+    [KEY_POWER] = "KEY_POWER",
+    [KEY_KPEQUAL] = "KEY_KPEQUAL",
+    [KEY_KPPLUSMINUS] = "KEY_KPPLUSMINUS",
+    [KEY_PAUSE] = "KEY_PAUSE",
+    [KEY_SCALE] = "KEY_SCALE",
+    [KEY_KPCOMMA] = "KEY_KPCOMMA",
+    [KEY_HANGEUL] = "KEY_HANGEUL",
+    [KEY_HANGUEL] = "KEY_HANGUEL",
+    [KEY_HANJA] = "KEY_HANJA",
+    [KEY_YEN] = "KEY_YEN",
+    [KEY_LEFTMETA] = "KEY_LEFTMETA",
+    [KEY_RIGHTMETA] = "KEY_RIGHTMETA",
+    [KEY_COMPOSE] = "KEY_COMPOSE",
+    [KEY_STOP] = "KEY_STOP",
+    [KEY_AGAIN] = "KEY_AGAIN",
+    [KEY_PROPS] = "KEY_PROPS",
+    [KEY_UNDO] = "KEY_UNDO",
+    [KEY_FRONT] = "KEY_FRONT",
+    [KEY_COPY] = "KEY_COPY",
+    [KEY_OPEN] = "KEY_OPEN",
+    [KEY_PASTE] = "KEY_PASTE",
+    [KEY_FIND] = "KEY_FIND",
+    [KEY_CUT] = "KEY_CUT",
+    [KEY_HELP] = "KEY_HELP",
+    [KEY_MENU] = "KEY_MENU",
+    [KEY_CALC] = "KEY_CALC",
+    [KEY_SETUP] = "KEY_SETUP",
+    [KEY_SLEEP] = "KEY_SLEEP",
+    [KEY_WAKEUP] = "KEY_WAKEUP",
+    [KEY_FILE] = "KEY_FILE",
+    [KEY_SENDFILE] = "KEY_SENDFILE",
+    [KEY_DELETEFILE] = "KEY_DELETEFILE",
+    [KEY_XFER] = "KEY_XFER",
+    [KEY_PROG1] = "KEY_PROG1",
+    [KEY_PROG2] = "KEY_PROG2",
+    [KEY_WWW] = "KEY_WWW",
+    [KEY_MSDOS] = "KEY_MSDOS",
+    [KEY_COFFEE] = "KEY_COFFEE",
+    [KEY_SCREENLOCK] = "KEY_SCREENLOCK",
+    [KEY_DIRECTION] = "KEY_DIRECTION",
+    [KEY_CYCLEWINDOWS] = "KEY_CYCLEWINDOWS",
+    [KEY_MAIL] = "KEY_MAIL",
+    [KEY_BOOKMARKS] = "KEY_BOOKMARKS",
+    [KEY_COMPUTER] = "KEY_COMPUTER",
+    [KEY_BACK] = "KEY_BACK",
+    [KEY_FORWARD] = "KEY_FORWARD",
+    [KEY_CLOSECD] = "KEY_CLOSECD",
+    [KEY_EJECTCD] = "KEY_EJECTCD",
+    [KEY_EJECTCLOSECD] = "KEY_EJECTCLOSECD",
+    [KEY_NEXTSONG] = "KEY_NEXTSONG",
+    [KEY_PLAYPAUSE] = "KEY_PLAYPAUSE",
+    [KEY_PREVIOUSSONG] = "KEY_PREVIOUSSONG",
+    [KEY_STOPCD] = "KEY_STOPCD",
+    [KEY_RECORD] = "KEY_RECORD",
+    [KEY_REWIND] = "KEY_REWIND",
+    [KEY_PHONE] = "KEY_PHONE",
+    [KEY_ISO] = "KEY_ISO",
+    [KEY_CONFIG] = "KEY_CONFIG",
+    [KEY_HOMEPAGE] = "KEY_HOMEPAGE",
+    [KEY_REFRESH] = "KEY_REFRESH",
+    [KEY_EXIT] = "KEY_EXIT",
+    [KEY_MOVE] = "KEY_MOVE",
+    [KEY_EDIT] = "KEY_EDIT",
+    [KEY_SCROLLUP] = "KEY_SCROLLUP",
+    [KEY_SCROLLDOWN] = "KEY_SCROLLDOWN",
+    [KEY_KPLEFTPAREN] = "KEY_KPLEFTPAREN",
+    [KEY_KPRIGHTPAREN] = "KEY_KPRIGHTPAREN",
+    [KEY_NEW] = "KEY_NEW",
+    [KEY_REDO] = "KEY_REDO",
+    [KEY_F13] = "KEY_F13",
+    [KEY_F14] = "KEY_F14",
+    [KEY_F15] = "KEY_F15",
+    [KEY_F16] = "KEY_F16",
+    [KEY_F17] = "KEY_F17",
+    [KEY_F18] = "KEY_F18",
+    [KEY_F19] = "KEY_F19",
+    [KEY_F20] = "KEY_F20",
+    [KEY_F21] = "KEY_F21",
+    [KEY_F22] = "KEY_F22",
+    [KEY_F23] = "KEY_F23",
+    [KEY_F24] = "KEY_F24",
+    [KEY_PLAYCD] = "KEY_PLAYCD",
+    [KEY_PAUSECD] = "KEY_PAUSECD",
+    [KEY_PROG3] = "KEY_PROG3",
+    [KEY_PROG4] = "KEY_PROG4",
+    [KEY_DASHBOARD] = "KEY_DASHBOARD",
+    [KEY_SUSPEND] = "KEY_SUSPEND",
+    [KEY_CLOSE] = "KEY_CLOSE",
+    [KEY_PLAY] = "KEY_PLAY",
+    [KEY_FASTFORWARD] = "KEY_FASTFORWARD",
+    [KEY_BASSBOOST] = "KEY_BASSBOOST",
+    [KEY_PRINT] = "KEY_PRINT",
+    [KEY_HP] = "KEY_HP",
+    [KEY_CAMERA] = "KEY_CAMERA",
+    [KEY_SOUND] = "KEY_SOUND",
+    [KEY_QUESTION] = "KEY_QUESTION",
+    [KEY_EMAIL] = "KEY_EMAIL",
+    [KEY_CHAT] = "KEY_CHAT",
+    [KEY_SEARCH] = "KEY_SEARCH",
+    [KEY_CONNECT] = "KEY_CONNECT",
+    [KEY_FINANCE] = "KEY_FINANCE",
+    [KEY_SPORT] = "KEY_SPORT",
+    [KEY_SHOP] = "KEY_SHOP",
+    [KEY_ALTERASE] = "KEY_ALTERASE",
+    [KEY_CANCEL] = "KEY_CANCEL",
+    [KEY_BRIGHTNESSDOWN] = "KEY_BRIGHTNESSDOWN",
+    [KEY_BRIGHTNESSUP] = "KEY_BRIGHTNESSUP",
+    [KEY_MEDIA] = "KEY_MEDIA",
+    [KEY_SWITCHVIDEOMODE] = "KEY_SWITCHVIDEOMODE",
+    [KEY_KBDILLUMTOGGLE] = "KEY_KBDILLUMTOGGLE",
+    [KEY_KBDILLUMDOWN] = "KEY_KBDILLUMDOWN",
+    [KEY_KBDILLUMUP] = "KEY_KBDILLUMUP",
+    [KEY_SEND] = "KEY_SEND",
+    [KEY_REPLY] = "KEY_REPLY",
+    [KEY_FORWARDMAIL] = "KEY_FORWARDMAIL",
+    [KEY_SAVE] = "KEY_SAVE",
+    [KEY_DOCUMENTS] = "KEY_DOCUMENTS",
+    [KEY_BATTERY] = "KEY_BATTERY",
+    [KEY_BLUETOOTH] = "KEY_BLUETOOTH",
+    [KEY_WLAN] = "KEY_WLAN",
+    [KEY_UWB] = "KEY_UWB",
+    [KEY_UNKNOWN] = "KEY_UNKNOWN",
+    [KEY_VIDEO_NEXT] = "KEY_VIDEO_NEXT",
+    [KEY_VIDEO_PREV] = "KEY_VIDEO_PREV",
+    [KEY_BRIGHTNESS_CYCLE] = "KEY_BRIGHTNESS_CYCLE",
+    [KEY_BRIGHTNESS_ZERO] = "KEY_BRIGHTNESS_ZERO",
+    [KEY_DISPLAY_OFF] = "KEY_DISPLAY_OFF",
+    [KEY_WIMAX] = "KEY_WIMAX",
+    [KEY_OK] = "KEY_OK",
+    [KEY_SELECT] = "KEY_SELECT",
+    [KEY_GOTO] = "KEY_GOTO",
+    [KEY_CLEAR] = "KEY_CLEAR",
+    [KEY_POWER2] = "KEY_POWER2",
+    [KEY_OPTION] = "KEY_OPTION",
+    [KEY_INFO] = "KEY_INFO",
+    [KEY_TIME] = "KEY_TIME",
+    [KEY_VENDOR] = "KEY_VENDOR",
+    [KEY_ARCHIVE] = "KEY_ARCHIVE",
+    [KEY_PROGRAM] = "KEY_PROGRAM",
+    [KEY_CHANNEL] = "KEY_CHANNEL",
+    [KEY_FAVORITES] = "KEY_FAVORITES",
+    [KEY_EPG] = "KEY_EPG",
+    [KEY_PVR] = "KEY_PVR",
+    [KEY_MHP] = "KEY_MHP",
+    [KEY_LANGUAGE] = "KEY_LANGUAGE",
+    [KEY_TITLE] = "KEY_TITLE",
+    [KEY_SUBTITLE] = "KEY_SUBTITLE",
+    [KEY_ANGLE] = "KEY_ANGLE",
+    [KEY_ZOOM] = "KEY_ZOOM",
+    [KEY_MODE] = "KEY_MODE",
+    [KEY_KEYBOARD] = "KEY_KEYBOARD",
+    [KEY_SCREEN] = "KEY_SCREEN",
+    [KEY_PC] = "KEY_PC",
+    [KEY_TV] = "KEY_TV",
+    [KEY_TV2] = "KEY_TV2",
+    [KEY_VCR] = "KEY_VCR",
+    [KEY_VCR2] = "KEY_VCR2",
+    [KEY_SAT] = "KEY_SAT",
+    [KEY_SAT2] = "KEY_SAT2",
+    [KEY_CD] = "KEY_CD",
+    [KEY_TAPE] = "KEY_TAPE",
+    [KEY_RADIO] = "KEY_RADIO",
+    [KEY_TUNER] = "KEY_TUNER",
+    [KEY_PLAYER] = "KEY_PLAYER",
+    [KEY_TEXT] = "KEY_TEXT",
+    [KEY_DVD] = "KEY_DVD",
+    [KEY_AUX] = "KEY_AUX",
+    [KEY_MP3] = "KEY_MP3",
+    [KEY_AUDIO] = "KEY_AUDIO",
+    [KEY_VIDEO] = "KEY_VIDEO",
+    [KEY_DIRECTORY] = "KEY_DIRECTORY",
+    [KEY_LIST] = "KEY_LIST",
+    [KEY_MEMO] = "KEY_MEMO",
+    [KEY_CALENDAR] = "KEY_CALENDAR",
+    [KEY_RED] = "KEY_RED",
+    [KEY_GREEN] = "KEY_GREEN",
+    [KEY_YELLOW] = "KEY_YELLOW",
+    [KEY_BLUE] = "KEY_BLUE",
+    [KEY_CHANNELUP] = "KEY_CHANNELUP",
+    [KEY_CHANNELDOWN] = "KEY_CHANNELDOWN",
+    [KEY_FIRST] = "KEY_FIRST",
+    [KEY_LAST] = "KEY_LAST",
+    [KEY_AB] = "KEY_AB",
+    [KEY_NEXT] = "KEY_NEXT",
+    [KEY_RESTART] = "KEY_RESTART",
+    [KEY_SLOW] = "KEY_SLOW",
+    [KEY_SHUFFLE] = "KEY_SHUFFLE",
+    [KEY_BREAK] = "KEY_BREAK",
+    [KEY_PREVIOUS] = "KEY_PREVIOUS",
+    [KEY_DIGITS] = "KEY_DIGITS",
+    [KEY_TEEN] = "KEY_TEEN",
+    [KEY_TWEN] = "KEY_TWEN",
+    [KEY_VIDEOPHONE] = "KEY_VIDEOPHONE",
+    [KEY_GAMES] = "KEY_GAMES",
+    [KEY_ZOOMIN] = "KEY_ZOOMIN",
+    [KEY_ZOOMOUT] = "KEY_ZOOMOUT",
+    [KEY_ZOOMRESET] = "KEY_ZOOMRESET",
+    [KEY_WORDPROCESSOR] = "KEY_WORDPROCESSOR",
+    [KEY_EDITOR] = "KEY_EDITOR",
+    [KEY_SPREADSHEET] = "KEY_SPREADSHEET",
+    [KEY_GRAPHICSEDITOR] = "KEY_GRAPHICSEDITOR",
+    [KEY_PRESENTATION] = "KEY_PRESENTATION",
+    [KEY_DATABASE] = "KEY_DATABASE",
+    [KEY_NEWS] = "KEY_NEWS",
+    [KEY_VOICEMAIL] = "KEY_VOICEMAIL",
+    [KEY_ADDRESSBOOK] = "KEY_ADDRESSBOOK",
+    [KEY_MESSENGER] = "KEY_MESSENGER",
+    [KEY_DISPLAYTOGGLE] = "KEY_DISPLAYTOGGLE",
+    [KEY_SPELLCHECK] = "KEY_SPELLCHECK",
+    [KEY_LOGOFF] = "KEY_LOGOFF",
+    [KEY_DOLLAR] = "KEY_DOLLAR",
+    [KEY_EURO] = "KEY_EURO",
+    [KEY_FRAMEBACK] = "KEY_FRAMEBACK",
+    [KEY_FRAMEFORWARD] = "KEY_FRAMEFORWARD",
+    [KEY_CONTEXT_MENU] = "KEY_CONTEXT_MENU",
+    [KEY_MEDIA_REPEAT] = "KEY_MEDIA_REPEAT",
+    [KEY_10CHANNELSUP] = "KEY_10CHANNELSUP",
+    [KEY_10CHANNELSDOWN] = "KEY_10CHANNELSDOWN",
+    [KEY_DEL_EOL] = "KEY_DEL_EOL",
+    [KEY_DEL_EOS] = "KEY_DEL_EOS",
+    [KEY_INS_LINE] = "KEY_INS_LINE",
+    [KEY_DEL_LINE] = "KEY_DEL_LINE",
+    [KEY_FN] = "KEY_FN",
+    [KEY_FN_ESC] = "KEY_FN_ESC",
+    [KEY_FN_F1] = "KEY_FN_F1",
+    [KEY_FN_F2] = "KEY_FN_F2",
+    [KEY_FN_F3] = "KEY_FN_F3",
+    [KEY_FN_F4] = "KEY_FN_F4",
+    [KEY_FN_F5] = "KEY_FN_F5",
+    [KEY_FN_F6] = "KEY_FN_F6",
+    [KEY_FN_F7] = "KEY_FN_F7",
+    [KEY_FN_F8] = "KEY_FN_F8",
+    [KEY_FN_F9] = "KEY_FN_F9",
+    [KEY_FN_F10] = "KEY_FN_F10",
+    [KEY_FN_F11] = "KEY_FN_F11",
+    [KEY_FN_F12] = "KEY_FN_F12",
+    [KEY_FN_1] = "KEY_FN_1",
+    [KEY_FN_2] = "KEY_FN_2",
+    [KEY_FN_D] = "KEY_FN_D",
+    [KEY_FN_E] = "KEY_FN_E",
+    [KEY_FN_F] = "KEY_FN_F",
+    [KEY_FN_S] = "KEY_FN_S",
+    [KEY_FN_B] = "KEY_FN_B",
+    [KEY_BRL_DOT1] = "KEY_BRL_DOT1",
+    [KEY_BRL_DOT2] = "KEY_BRL_DOT2",
+    [KEY_BRL_DOT3] = "KEY_BRL_DOT3",
+    [KEY_BRL_DOT4] = "KEY_BRL_DOT4",
+    [KEY_BRL_DOT5] = "KEY_BRL_DOT5",
+    [KEY_BRL_DOT6] = "KEY_BRL_DOT6",
+    [KEY_BRL_DOT7] = "KEY_BRL_DOT7",
+    [KEY_BRL_DOT8] = "KEY_BRL_DOT8",
+    [KEY_BRL_DOT9] = "KEY_BRL_DOT9",
+    [KEY_BRL_DOT10] = "KEY_BRL_DOT10",
+    [KEY_NUMERIC_0] = "KEY_NUMERIC_0",
+    [KEY_NUMERIC_1] = "KEY_NUMERIC_1",
+    [KEY_NUMERIC_2] = "KEY_NUMERIC_2",
+    [KEY_NUMERIC_3] = "KEY_NUMERIC_3",
+    [KEY_NUMERIC_4] = "KEY_NUMERIC_4",
+    [KEY_NUMERIC_5] = "KEY_NUMERIC_5",
+    [KEY_NUMERIC_6] = "KEY_NUMERIC_6",
+    [KEY_NUMERIC_7] = "KEY_NUMERIC_7",
+    [KEY_NUMERIC_8] = "KEY_NUMERIC_8",
+    [KEY_NUMERIC_9] = "KEY_NUMERIC_9",
+    [KEY_NUMERIC_STAR] = "KEY_NUMERIC_STAR",
+    [KEY_NUMERIC_POUND] = "KEY_NUMERIC_POUND",
+    [KEY_RFKILL] = "KEY_RFKILL",
+    [KEY_MIN_INTERESTING] = "KEY_MIN_INTERESTING",
+    [KEY_MAX] = "KEY_MAX",
+    [KEY_CNT] = "KEY_CNT",
diff --git a/vl.c b/vl.c
index 47ab45d..aa688d5 100644
--- a/vl.c
+++ b/vl.c
@@ -2241,6 +2241,13 @@ static DisplayType select_display(const char *p)
         fprintf(stderr, "GTK support is disabled\n");
         exit(1);
 #endif
+    } else if (strstart(p, "fbdev", &opts)) {
+#ifdef CONFIG_FBDEV
+        display = DT_FBDEV;
+#else
+        fprintf(stderr, "fbdev support is disabled\n");
+        exit(1);
+#endif
     } else if (strstart(p, "none", &opts)) {
         display = DT_NONE;
     } else {
@@ -4336,6 +4343,20 @@ int main(int argc, char **argv, char **envp)
         curses_display_init(ds, full_screen);
         break;
 #endif
+#if defined(CONFIG_FBDEV)
+    case DT_FBDEV:
+    {
+        Error *errp = NULL;
+        if (fbdev_display_init(NULL, false, &errp) != 0) {
+            if (error_is_set(&errp)) {
+                fprintf(stderr, "%s\n", error_get_pretty(errp));
+                error_free(errp);
+            }
+            exit(1);
+        }
+        break;
+    }
+#endif
 #if defined(CONFIG_SDL)
     case DT_SDL:
         sdl_display_init(ds, full_screen, no_frame);
