diff mbox

[5/5] linux fbdev display driver.

Message ID 1276596347-9410-6-git-send-email-kraxel@redhat.com
State New
Headers show

Commit Message

Gerd Hoffmann June 15, 2010, 10:05 a.m. UTC
Display works, requires truecolor framebuffer with 16 or 32 bpp on the
host.  32bpp is recommended.  The framebuffer is used as-is, qemu
doesn't try to switch modes.  With LCD displays mode switching is pretty
pointless IMHO, also it wouldn't work anyway with the most common
fbdev drivers (vesafb, KMS).  Guest display is centered on the host
screen.

Mouse works, uses /dev/input/mice.

Keyboard works.  Guest screen has whatever keymap you load inside
the guest.  Text windows (monitor, serial, ...) have a simple en-us
keymap.  Good enougth to type monitor commands.  Not goot enougth to
work seriously on a serial terminal.  But the qemu terminal emulation
isn't good enougth for that anyway ;)

Hot keys:
  Ctrl-Alt-F<nr>  -> host console switching.
  Ctrl-Alt-<nr>   -> qemu console switching.
  Ctrl-Alt-ESC    -> exit qemu.

Special feature:  Sane console switching.  Switching away stops screen
updates.  Switching back redraws the screen.  When started from the
linux console qemu uses the vt you've started it from (requires just
read/write access to /dev/fb0).  When starting from somewhere else qemu
tries to open a unused virtual terminal and switch to it (usually
requires root privileges to open /dev/tty<nr>).

For some strange reason console switching from X11 to qemu doesn't work.
Anything else (including X11 -> text console -> qemu) works fine.  To be
investigated ...

Can be enabled/disabled via monitor, use "change fbdev [ on | off ]"

Cc: Julian Pidancet <julian.pidancet@citrix.com>
Cc: Stefano Stabellini <stefano.stabellini@eu.citrix.com>
Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 Makefile.objs    |    1 +
 console.h        |    4 +
 fbdev.c          |  931 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 linux-keynames.h |  386 ++++++++++++++++++++++
 monitor.c        |   14 +
 qemu-options.hx  |   10 +
 sysemu.h         |    1 +
 vl.c             |   10 +
 8 files changed, 1357 insertions(+), 0 deletions(-)
 create mode 100644 fbdev.c
 create mode 100644 linux-keynames.h

Comments

Stefano Stabellini June 16, 2010, 12:44 p.m. UTC | #1
On Tue, 15 Jun 2010, Gerd Hoffmann wrote:
> Display works, requires truecolor framebuffer with 16 or 32 bpp on the
> host.  32bpp is recommended.  The framebuffer is used as-is, qemu
> doesn't try to switch modes.  With LCD displays mode switching is pretty
> pointless IMHO, also it wouldn't work anyway with the most common
> fbdev drivers (vesafb, KMS).  Guest display is centered on the host
> screen.
> 
> Mouse works, uses /dev/input/mice.
> 
> Keyboard works.  Guest screen has whatever keymap you load inside
> the guest.  Text windows (monitor, serial, ...) have a simple en-us
> keymap.  Good enougth to type monitor commands.  Not goot enougth to
> work seriously on a serial terminal.  But the qemu terminal emulation
> isn't good enougth for that anyway ;)
> 
> Hot keys:
>   Ctrl-Alt-F<nr>  -> host console switching.
>   Ctrl-Alt-<nr>   -> qemu console switching.
>   Ctrl-Alt-ESC    -> exit qemu.
> 
> Special feature:  Sane console switching.  Switching away stops screen
> updates.  Switching back redraws the screen.  When started from the
> linux console qemu uses the vt you've started it from (requires just
> read/write access to /dev/fb0).  When starting from somewhere else qemu
> tries to open a unused virtual terminal and switch to it (usually
> requires root privileges to open /dev/tty<nr>).
> 
> For some strange reason console switching from X11 to qemu doesn't work.
> Anything else (including X11 -> text console -> qemu) works fine.  To be
> investigated ...
> 
> Can be enabled/disabled via monitor, use "change fbdev [ on | off ]"

the patch still doesn't use the display allocator interface, but it
shouldn't be difficult to implement support for it on top of this patch,
so it is fine by me.
diff mbox

Patch

diff --git a/Makefile.objs b/Makefile.objs
index acc6179..be6a03c 100644
--- a/Makefile.objs
+++ b/Makefile.objs
@@ -106,6 +106,7 @@  common-obj-y += $(addprefix audio/, $(audio-obj-y))
 common-obj-y += keymaps.o
 common-obj-$(CONFIG_SDL) += sdl.o sdl_zoom.o x_keymap.o
 common-obj-$(CONFIG_CURSES) += curses.o
+common-obj-$(CONFIG_LINUX) += fbdev.o
 common-obj-y += vnc.o acl.o d3des.o
 common-obj-y += vnc-encoding-zlib.o vnc-encoding-hextile.o
 common-obj-y += vnc-encoding-tight.o
diff --git a/console.h b/console.h
index f866987..124a22b 100644
--- a/console.h
+++ b/console.h
@@ -369,6 +369,10 @@  void qemu_console_copy(DisplayState *ds, int src_x, int src_y,
 /* sdl.c */
 void sdl_display_init(DisplayState *ds, int full_screen, int no_frame);
 
+/* fbdev.c */
+void fbdev_display_init(DisplayState *ds, const char *device);
+void fbdev_display_uninit(void);
+
 /* cocoa.m */
 void cocoa_display_init(DisplayState *ds, int full_screen);
 
diff --git a/fbdev.c b/fbdev.c
new file mode 100644
index 0000000..2ac4aba
--- /dev/null
+++ b/fbdev.c
@@ -0,0 +1,931 @@ 
+/*
+ * linux fbdev output driver.
+ *
+ * Author: Gerd Hoffmann <kraxel@redhat.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2.  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 <linux/input.h>
+
+#include "qemu-common.h"
+#include "console.h"
+#include "keymaps.h"
+#include "sysemu.h"
+#include "pflib.h"
+
+/* -------------------------------------------------------------------- */
+
+/* file handles */
+static int                        tty = -1, fb = -1, mice = -1;
+
+/* saved state, for restore on exit */
+static int                        orig_vtno = 0;
+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 = 0;
+
+/* 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 = FB_ACTIVE;
+
+/* qdev windup */
+static DisplayChangeListener      *dcl;
+static QemuPfConv                 *conv;
+static PixelFormat                fbpf;
+static int                        resize_screen;
+static int                        redraw_screen;
+static int                        cx, cy, cw, ch;
+static int                        debug = 0;
+static Notifier                   exit_notifier;
+
+/* fwd decls */
+static int fbdev_activate_vt(int tty, int vtno, bool wait);
+
+/* -------------------------------------------------------------------- */
+/* 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)
+            ax = cw-1;
+        if (ay >= ch)
+            ay = ch-1;
+        kbd_mouse_event(ax * 0x7FFF / cw, ay * 0x7FFF / ch, 0, b);
+    } else {
+        kbd_mouse_event(x, y, 0, b);
+    }
+}
+
+static int init_mouse(void)
+{
+    mice = open("/dev/input/mice", O_RDONLY);
+    if (-1 == mice)
+        return -1;
+    qemu_set_fd_handler(mice, read_mouse, NULL, NULL);
+    return 0;
+}
+
+static void uninit_mouse(void)
+{
+    if (-1 == mice)
+        return;
+    qemu_set_fd_handler(mice, NULL, NULL, NULL);
+    close(mice);
+    mice = -1;
+}
+
+/* -------------------------------------------------------------------- */
+/* keyboard                                                             */
+
+static const char *keynames[] = {
+#include "linux-keynames.h"
+};
+
+static 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,
+//    [ KEY_PAUSE        ] = FIXME,
+    [ 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 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;
+    if (debug)
+        fprintf(stderr, "%s\n", __FUNCTION__);
+
+    /* 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;
+    if (debug)
+        fprintf(stderr, "%s\n", __FUNCTION__);
+
+    /* 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",
+                __FUNCTION__, 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)
+{
+    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",
+                __FUNCTION__, 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 (is_graphic_console())
+                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", __FUNCTION__);
+        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;
+            }
+
+            if (debug)
+                fprintf(stderr, "%s: 0x%x %s %s\n", __FUNCTION__,
+                        keycode, keynames[keycode], up ? "up" : "down");
+
+            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_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 (is_graphic_console()) {
+                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)
+{
+    if (debug) {
+        fprintf(stderr, "%s: %d (%s)\n", __FUNCTION__,
+                vtno, wait ? "wait" : "nowait");
+    }
+
+    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;
+        }
+        if (debug) {
+            fprintf(stderr, "%s: finished\n", __FUNCTION__);
+        }
+    }
+    return 0;
+}
+
+static void fbdev_cleanup(void)
+{
+    if (debug)
+        fprintf(stderr, "%s\n", __FUNCTION__);
+
+    /* restore console */
+    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)
+{
+    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) {
+	fprintf(stderr, "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) {
+            fprintf(stderr, "open %s: %s\n", ttyname, strerror(errno));
+            exit(1);
+        }
+        if (ioctl(tty,VT_OPENQRY, &vtno) < 0) {
+	    perror("ioctl VT_OPENQRY");
+	    exit(1);
+	}
+        if (ioctl(tty,VT_GETSTATE, &vts) < 0) {
+            perror("ioctl VT_GETSTATE\n");
+            exit(1);
+        }
+        close(tty);
+
+        snprintf(ttyname, sizeof(ttyname), "/dev/tty%d", vtno);
+        tty = open(ttyname, O_RDWR);
+        if (tty == -1) {
+            fprintf(stderr, "open %s: %s\n", ttyname, strerror(errno));
+            exit(1);
+        }
+        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) {
+	perror("ioctl FBIOGET_VSCREENINFO");
+        return -1;
+    }
+    if (ioctl(tty, KDGETMODE, &kd_omode) < 0) {
+	perror("ioctl KDGETMODE");
+        return -1;
+    }
+    if (ioctl(tty, VT_GETMODE, &vt_omode) < 0) {
+	perror("ioctl VT_GETMODE");
+        return -1;
+    }
+
+    /* checks & initialisation */
+    if (ioctl(fb,FBIOGET_FSCREENINFO,&fb_fix) < 0) {
+	perror("ioctl FBIOGET_FSCREENINFO");
+	goto err;
+    }
+    if (ioctl(fb,FBIOGET_VSCREENINFO,&fb_var) < 0) {
+	perror("ioctl FBIOGET_VSCREENINFO");
+	goto err;
+    }
+    if (fb_fix.type != FB_TYPE_PACKED_PIXELS) {
+	fprintf(stderr, "can handle only packed pixel frame buffers\n");
+	goto err;
+    }
+    switch (fb_var.bits_per_pixel) {
+    case 32:
+        break;
+    default:
+	fprintf(stderr, "can't handle %d bpp frame buffers\n",
+                fb_var.bits_per_pixel);
+	goto err;
+    }
+
+    page_mask = getpagesize()-1;
+    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) {
+	perror("mmap");
+	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) {
+	    perror("ioctl FBIOPAN_DISPLAY");
+	    goto err;
+	}
+    }
+    if (ioctl(tty,KDSETMODE, KD_GRAPHICS) < 0) {
+	perror("ioctl KDSETMODE");
+	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);
+
+    /* create PixelFormat from fbdev structs */
+    fbpf.bits_per_pixel  = fb_var.bits_per_pixel;
+    fbpf.bytes_per_pixel = (fb_var.bits_per_pixel+7)/8;
+    fbpf.depth           = fb_var.bits_per_pixel == 32 ? 24 : fb_var.bits_per_pixel;
+    fbpf.rshift          = fb_var.red.offset;
+    fbpf.rbits           = fb_var.red.length;
+    fbpf.gshift          = fb_var.green.offset;
+    fbpf.gbits           = fb_var.green.length;
+    fbpf.bshift          = fb_var.blue.offset;
+    fbpf.bbits           = fb_var.blue.length;
+    fbpf.ashift          = fb_var.transp.offset;
+    fbpf.abits           = fb_var.transp.length;
+
+    if (fbpf.rbits) {
+        fbpf.rmax   = (1 << fbpf.rbits) - 1;
+        fbpf.rmask  = fbpf.rmax << fbpf.rshift;
+    }
+    if (fbpf.gbits) {
+        fbpf.gmax   = (1 << fbpf.gbits) - 1;
+        fbpf.gmask  = fbpf.gmax << fbpf.gshift;
+    }
+    if (fbpf.bbits) {
+        fbpf.bmax   = (1 << fbpf.bbits) - 1;
+        fbpf.bmask  = fbpf.bmax << fbpf.bshift;
+    }
+    if (fbpf.abits) {
+        fbpf.amax   = (1 << fbpf.abits) - 1;
+        fbpf.amask  = fbpf.amax << fbpf.ashift;
+    }
+    return 0;
+
+ err:
+    fbdev_cleanup();
+    return -1;
+}
+
+static void
+fbdev_catch_fatal_signal(int signal)
+{
+    fbdev_cleanup();
+    abort();
+}
+
+static void fbdev_catch_exit_signals(void)
+{
+    struct sigaction act,old;
+
+    memset(&act,0,sizeof(act));
+    act.sa_handler = fbdev_catch_fatal_signal;
+    sigemptyset(&act.sa_mask);
+    sigaction(SIGSEGV,&act,&old);
+}
+
+/* -------------------------------------------------------------------- */
+/* console switching                                                    */
+
+static void fbdev_switch_signal(int signal)
+{
+    if (signal == SIG_REL) {
+	/* release */
+        if (debug)
+            fprintf(stderr, "%s: release request\n", __FUNCTION__);
+	fb_switch_state = FB_REL_REQ;
+    }
+    if (signal == SIG_ACQ) {
+	/* acquisition */
+        if (debug)
+            fprintf(stderr, "%s: aquire request\n", __FUNCTION__);
+	fb_switch_state = FB_ACQ_REQ;
+    }
+}
+
+static void fbdev_switch_release(void)
+{
+    if (debug)
+        fprintf(stderr, "%s\n", __FUNCTION__);
+    stop_mediumraw(tty);
+    ioctl(tty, VT_RELDISP, 1);
+    fb_switch_state = FB_INACTIVE;
+}
+
+static void fbdev_switch_acquire(void)
+{
+    if (debug)
+        fprintf(stderr, "%s\n", __FUNCTION__);
+    ioctl(tty, VT_RELDISP, VT_ACKACQ);
+    start_mediumraw(tty);
+    reset_keys();
+    fb_switch_state = FB_ACTIVE;
+}
+
+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(DisplayState *ds, int x, int y, int w, int h)
+{
+    uint8_t *dst;
+    uint8_t *src;
+    int line;
+
+    if (!conv)
+        return;
+
+    src = ds_get_data(ds) + y * ds_get_linesize(ds)
+        + x * ds_get_bytes_per_pixel(ds);
+    dst = fb_mem + y * fb_fix.line_length
+        + x * fbpf.bytes_per_pixel;
+
+    dst += cy * fb_fix.line_length;
+    dst += cx * fbpf.bytes_per_pixel;
+
+    if (h > fb_var.yres - y) {
+        h = fb_var.yres - y;
+    }
+    if (w > fb_var.xres - x) {
+        w = fb_var.xres - x;
+    }
+
+    for (line = y; line < y+h; line++) {
+        qemu_pf_conv_run(conv, dst, src, w);
+        dst += fb_fix.line_length;
+        src += ds_get_linesize(ds);
+    }
+}
+
+/* -------------------------------------------------------------------- */
+/* qemu interfaces                                                      */
+
+static void fbdev_update(DisplayState *ds, int x, int y, int w, int h)
+{
+    if (fb_switch_state != FB_ACTIVE)
+        return;
+
+    if (resize_screen) {
+        if (debug)
+            fprintf(stderr, "%s: handle resize\n", __FUNCTION__);
+        resize_screen = 0;
+        cx = 0; cy = 0;
+        cw = ds_get_width(ds);
+        ch = ds_get_height(ds);
+        if (ds_get_width(ds) < fb_var.xres) {
+            cx = (fb_var.xres - ds_get_width(ds)) / 2;
+        }
+        if (ds_get_height(ds) < fb_var.yres) {
+            cy = (fb_var.yres - ds_get_height(ds)) / 2;
+        }
+
+        if (conv) {
+            qemu_pf_conv_put(conv);
+        }
+        conv = qemu_pf_conv_get(&fbpf, &ds->surface->pf);
+        if (conv == NULL) {
+            fprintf(stderr, "fbdev: unsupported PixelFormat conversion\n");
+        }
+    }
+
+    if (redraw_screen) {
+        if (debug)
+            fprintf(stderr, "%s: handle redraw\n", __FUNCTION__);
+        redraw_screen = 0;
+        fbdev_cls();
+        x = 0; y = 0; w = ds_get_width(ds); h = ds_get_height(ds);
+    }
+
+    fbdev_render(ds, x, y, w, h);
+}
+
+static void fbdev_resize(DisplayState *ds)
+{
+    if (debug)
+        fprintf(stderr, "%s: request resize+redraw\n", __FUNCTION__);
+    resize_screen++;
+    redraw_screen++;
+}
+
+static void fbdev_refresh(DisplayState *ds)
+{
+    switch (fb_switch_state) {
+    case FB_REL_REQ:
+        fbdev_switch_release();
+    case FB_INACTIVE:
+        return;
+    case FB_ACQ_REQ:
+        fbdev_switch_acquire();
+        redraw_screen++;
+        if (debug)
+            fprintf(stderr, "%s: request redraw\n", __FUNCTION__);
+    case FB_ACTIVE:
+        break;
+    }
+
+    vga_hw_update();
+    if (redraw_screen) {
+        fbdev_update(ds, 0, 0, 0, 0);
+    }
+}
+
+static void fbdev_exit_notifier(Notifier *notifier)
+{
+    fbdev_cleanup();
+}
+
+void fbdev_display_init(DisplayState *ds, const char *device)
+{
+    if (dcl != NULL) {
+        if (debug)
+            fprintf(stderr, "%s: already active\n", __FUNCTION__);
+        return;
+    }
+
+    if (fbdev_init(device) != 0) {
+        exit(1);
+    }
+    exit_notifier.notify = fbdev_exit_notifier;
+    qemu_add_exit_notifier(&exit_notifier);
+    fbdev_switch_init();
+    fbdev_catch_exit_signals();
+    init_mouse();
+
+    fbdev_resize(ds);
+    dcl = qemu_mallocz(sizeof(DisplayChangeListener));
+    dcl->dpy_update  = fbdev_update;
+    dcl->dpy_resize  = fbdev_resize;
+    dcl->dpy_refresh = fbdev_refresh;
+    register_displaychangelistener(ds, dcl);
+}
+
+void fbdev_display_uninit(void)
+{
+    if (dcl == NULL) {
+        if (debug)
+            fprintf(stderr, "%s: not active\n", __FUNCTION__);
+        return;
+    }
+
+    unregister_displaychangelistener(dcl);
+    qemu_free(dcl);
+    dcl = NULL;
+
+    fbdev_cleanup();
+    qemu_remove_exit_notifier(&exit_notifier);
+    uninit_mouse();
+}
diff --git a/linux-keynames.h b/linux-keynames.h
new file mode 100644
index 0000000..5f08f02
--- /dev/null
+++ b/linux-keynames.h
@@ -0,0 +1,386 @@ 
+/*
+ *   awk '/#define KEY_/ { printf("    [ %-16s ] = \"%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_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/monitor.c b/monitor.c
index 05a7ed1..7442f1a 100644
--- a/monitor.c
+++ b/monitor.c
@@ -959,6 +959,18 @@  static int do_change_vnc(Monitor *mon, const char *target, const char *arg)
     return 0;
 }
 
+static int do_change_fbdev(Monitor *mon, const char *target)
+{
+#ifdef CONFIG_LINUX
+    if (strcmp(target, "on") == 0) {
+        fbdev_display_init(get_displaystate(), NULL);
+    } else {
+        fbdev_display_uninit();
+    }
+    return 0;
+#endif
+}
+
 /**
  * do_change(): Change a removable medium, or VNC configuration
  */
@@ -971,6 +983,8 @@  static int do_change(Monitor *mon, const QDict *qdict, QObject **ret_data)
 
     if (strcmp(device, "vnc") == 0) {
         ret = do_change_vnc(mon, target, arg);
+    } else if (strcmp(device, "fbdev") == 0) {
+        ret = do_change_fbdev(mon, target);
     } else {
         ret = do_change_block(mon, device, target, arg);
     }
diff --git a/qemu-options.hx b/qemu-options.hx
index a6928b7..8224a32 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -667,6 +667,16 @@  STEXI
 Enable SDL.
 ETEXI
 
+#ifdef CONFIG_LINUX
+DEF("fbdev", 0, QEMU_OPTION_fbdev,
+    "-fbdev          enable fbdev\n", QEMU_ARCH_ALL)
+#endif
+STEXI
+@item -fbdev
+@findex -fbdev
+Enable fbdev (linux framebuffer).
+ETEXI
+
 DEF("portrait", 0, QEMU_OPTION_portrait,
     "-portrait       rotate graphical output 90 deg left (only PXA LCD)\n",
     QEMU_ARCH_ALL)
diff --git a/sysemu.h b/sysemu.h
index c758243..e40fefd 100644
--- a/sysemu.h
+++ b/sysemu.h
@@ -95,6 +95,7 @@  typedef enum DisplayType
     DT_CURSES,
     DT_SDL,
     DT_VNC,
+    DT_FBDEV,
     DT_NOGRAPHIC,
 } DisplayType;
 
diff --git a/vl.c b/vl.c
index 8fc63c7..39e43fa 100644
--- a/vl.c
+++ b/vl.c
@@ -2400,6 +2400,11 @@  int main(int argc, char **argv, char **envp)
                 display_type = DT_SDL;
                 break;
 #endif
+#ifdef CONFIG_LINUX
+            case QEMU_OPTION_fbdev:
+                display_type = DT_FBDEV;
+                break;
+#endif
             case QEMU_OPTION_pidfile:
                 pid_file = optarg;
                 break;
@@ -2895,6 +2900,11 @@  int main(int argc, char **argv, char **envp)
         curses_display_init(ds, full_screen);
         break;
 #endif
+#if defined(CONFIG_LINUX)
+    case DT_FBDEV:
+        fbdev_display_init(ds, NULL);
+        break;
+#endif
 #if defined(CONFIG_SDL)
     case DT_SDL:
         sdl_display_init(ds, full_screen, no_frame);