Patchwork [[RfC] ] linux fbdev display driver prototype.

login
register
mail settings
Submitter Gerd Hoffmann
Date May 20, 2010, 8:20 p.m.
Message ID <1274386823-6153-1-git-send-email-kraxel@redhat.com>
Download mbox | patch
Permalink /patch/53105/
State New
Headers show

Comments

Gerd Hoffmann - May 20, 2010, 8:20 p.m.
Display works with 32 bpp (both host + guest) only.
Which surprisingly didn't cause much problems so far in my testing.
Host runs with kms and inteldrmfb.

Mouse support isn't available yet.
I've cheated by passed through the hosts usb mouse for testing.

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 ...

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        |    3 +
 fbdev.c          |  770 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 linux-keynames.h |  386 +++++++++++++++++++++++++++
 qemu-options.hx  |   10 +
 sysemu.h         |    1 +
 vl.c             |   10 +
 7 files changed, 1181 insertions(+), 0 deletions(-)
 create mode 100644 fbdev.c
 create mode 100644 linux-keynames.h
Stefano Stabellini - May 21, 2010, 10:11 a.m.
On Thu, 20 May 2010, Gerd Hoffmann wrote:

> Display works with 32 bpp (both host + guest) only.
> Which surprisingly didn't cause much problems so far in my testing.
> Host runs with kms and inteldrmfb.
> 
> Mouse support isn't available yet.
> I've cheated by passed through the hosts usb mouse for testing.
> 
> 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 ...
> 

Not bad for quick start!

Does qemu->qemu switching work when running multiple VMs?
Gerd Hoffmann - May 21, 2010, 10:26 a.m.
Hi,

> Does qemu->qemu switching work when running multiple VMs?

Yes.

cheers,
   Gerd
Julian Pidancet - May 21, 2010, 7:32 p.m.
On 05/20/2010 09:20 PM, Gerd Hoffmann wrote:
> Display works with 32 bpp (both host + guest) only.
> Which surprisingly didn't cause much problems so far in my testing.
> Host runs with kms and inteldrmfb.
> 
> Mouse support isn't available yet.
> I've cheated by passed through the hosts usb mouse for testing.
> 
> 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 ...
> 

This looks very promissing.

I just got a couple of observations:

- Your patch does not work on my machine with the vesafb driver. It reports "can't handle 8 bpp frame buffers". It turns out that the vesafb driver seems to initialize the framebuffer in PSEUDOCOLOR mode. I think we should add a piece of code which tries reinitialize the framebuffer with the suitable parametters (32bpp/TRUECOLOR). It works fine with inteldrmfb though.

- You should register a Display Allocator and override the create_displaysurface() method like I did in the DirectFB driver. This way you save qemu a data copy. fbdev_render_32() should only be used when the guest framebuffer is not compatible with the physical framebuffer (guest_bpp != physical_bbp || guest_linesize != physical_linesize).

- A cool feature would be to be able to stretch the guest display in fullscreen. My DirectFB driver implements a fullscreen toggle command by pressing the Ctrl-Alt-Return keys. I think Stefano added a SDL zoom feature a while ago which we could reuse for this.

- I'm not very familiar with the scancode stuff, but I think that if you set your VT fd in the K_RAW keyboard mode, you'll be able to get true keyboard scancodes that you can directly give to the guest using the kbd_put_keycode() function. This way we could avoid having to define keymaps and this scancode map inside qemu.

Cheers,

Julian
Stefano Stabellini - May 24, 2010, 10:05 a.m.
On Fri, 21 May 2010, Julian Pidancet wrote:
> This looks very promissing.
> 
> I just got a couple of observations:
> 
> - Your patch does not work on my machine with the vesafb driver. It reports "can't handle 8 bpp frame buffers". It turns out that the vesafb driver seems to initialize the framebuffer in PSEUDOCOLOR mode. I think we should add a piece of code which tries reinitialize the framebuffer with the suitable parametters (32bpp/TRUECOLOR). It works fine with inteldrmfb though.
> 

32bpp would be ideal, but we could probably handle 24 and 16 bpp too

> - You should register a Display Allocator and override the create_displaysurface() method like I did in the DirectFB driver. This way you save qemu a data copy. fbdev_render_32() should only be used when the guest framebuffer is not compatible with the physical framebuffer (guest_bpp != physical_bbp || guest_linesize != physical_linesize).
> 

agreed

> - A cool feature would be to be able to stretch the guest display in fullscreen. My DirectFB driver implements a fullscreen toggle command by pressing the Ctrl-Alt-Return keys. I think Stefano added a SDL zoom feature a while ago which we could reuse for this.
> 

sdl_zoom.c is actually generic and not SDL specific, so it shouldn't be
difficult to reuse it here
Gerd Hoffmann - May 25, 2010, 7:11 a.m.
> This looks very promissing.
>
> I just got a couple of observations:
>
> - Your patch does not work on my machine with the vesafb driver. It
> reports "can't handle 8 bpp frame buffers". It turns out that the
> vesafb driver seems to initialize the framebuffer in PSEUDOCOLOR
> mode.

Depends on the video mode you ask for via vga=$nr, there are also 32bpp 
modes.

> I think we should add a piece of code which tries reinitialize
> the framebuffer with the suitable parametters (32bpp/TRUECOLOR).

With vesafb it wouldn't work anyway, you can't switch these parameters 
at runtime.  I think the *drmfb fbdev interface is quite limited too in 
what it allows to change.

> - You should register a Display Allocator and override the
> create_displaysurface() method like I did in the DirectFB driver.
> This way you save qemu a data copy. fbdev_render_32() should only be
> used when the guest framebuffer is not compatible with the physical
> framebuffer (guest_bpp != physical_bbp || guest_linesize !=
> physical_linesize).

Isn't a trivial move though.  If the console is switched you must stop 
drawing on the framebuffer.

Right now this is easy: just stop copying.  Likewise restoring the 
screen when switching back is easy: just copy everything.

If we give out pointers to the framebuffer to other qemu code which 
doesn't know anything about console switching we have to be quite 
careful get things right ...

> - A cool feature would be to be able to stretch the guest display in
> fullscreen. My DirectFB driver implements a fullscreen toggle command
> by pressing the Ctrl-Alt-Return keys. I think Stefano added a SDL
> zoom feature a while ago which we could reuse for this.

The actual stretching is done by SDL I think.  For that kind of stuff a 
rendering library is actually helpful ...

> - I'm not very familiar with the scancode stuff, but I think that if
> you set your VT fd in the K_RAW keyboard mode, you'll be able to get
> true keyboard scancodes that you can directly give to the guest using
> the kbd_put_keycode() function.

I'm not sure this is really portable.  What do you get in K_RAW mode on 
!x86 platforms?  K_MEDIUMRAW gives you linux input layer key codes no 
matter what.  Also the translation to keysyms (for text consoles) is 
easier with mediumraw.

cheers,
   Gerd
Stefano Stabellini - May 25, 2010, 9:26 a.m.
On Tue, 25 May 2010, Gerd Hoffmann wrote:
> The actual stretching is done by SDL I think.  For that kind of stuff a 
> rendering library is actually helpful ...
> 

not really, the sdl_zoom* stuff is completely generic
Julian Pidancet - June 1, 2010, 11:11 a.m.
On 05/25/2010 08:11 AM, Gerd Hoffmann wrote:
>> - You should register a Display Allocator and override the
>> create_displaysurface() method like I did in the DirectFB driver.
>> This way you save qemu a data copy. fbdev_render_32() should only be
>> used when the guest framebuffer is not compatible with the physical
>> framebuffer (guest_bpp != physical_bbp || guest_linesize !=
>> physical_linesize).
> 
> Isn't a trivial move though.  If the console is switched you must stop 
> drawing on the framebuffer.
> 
> Right now this is easy: just stop copying.  Likewise restoring the 
> screen when switching back is easy: just copy everything.
> 
> If we give out pointers to the framebuffer to other qemu code which 
> doesn't know anything about console switching we have to be quite 
> careful get things right ...
> 

When loosing the focus, you can force display surface reallocation by calling hw_invalidate(), this way you can give qemu a temporary pointer to prevent drawing on the framebuffer.
When switching back, you just have to copy the content of the temporary pointer location to the framebuffer.


Cheers, 
  Julian Pidancet

Patch

diff --git a/Makefile.objs b/Makefile.objs
index ecdd53e..cff1a23 100644
--- a/Makefile.objs
+++ b/Makefile.objs
@@ -102,6 +102,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 += iov.o
diff --git a/console.h b/console.h
index 6def115..bba1da8 100644
--- a/console.h
+++ b/console.h
@@ -338,6 +338,9 @@  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);
+
 /* 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..9ad7db6
--- /dev/null
+++ b/fbdev.c
@@ -0,0 +1,770 @@ 
+#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"
+
+/* -------------------------------------------------------------------- */
+
+/* file handles */
+static int                        tty, fb;
+
+/* 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 int                        resize_screen;
+static int                        redraw_screen;
+static int                        cx, cy;
+static int                        debug = 0;
+
+/* fwd decls */
+static int fbdev_activate_vt(int tty, int vtno, bool wait);
+
+/* -------------------------------------------------------------------- */
+/* 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)
+{
+    /* FIXME: assumes 32 bpp */
+    memset(fb_mem+fb_mem_offset, 0x20, 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;
+        }
+    }
+    return 0;
+}
+
+static void fbdev_cleanup(void)
+{
+    if (debug)
+        fprintf(stderr, "%s\n", __FUNCTION__);
+
+    /* restore console */
+    if (ioctl(fb,FBIOPUT_VSCREENINFO, &fb_ovar) < 0)
+	perror("ioctl FBIOPUT_VSCREENINFO");
+    close(fb);
+
+    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);
+    close(tty);
+}
+
+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);
+    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_32(DisplayState *ds, int x, int y, int w, int h)
+{
+    int depth = ds_get_bits_per_pixel(ds);
+    uint8_t *dst;
+    uint8_t *src;
+    int line;
+
+    dst = fb_mem + fb_fix.line_length * y + 4 * x;
+    dst += cy * fb_fix.line_length;
+    dst += cx * 4;
+    switch (ds_get_bits_per_pixel(ds)) {
+    case 32:
+        src = ds_get_data(ds) + ds_get_linesize(ds) * y + 4 * x;
+        for (line = y; line < y+h; line++) {
+            memcpy(dst, src, w*4);
+            dst += fb_fix.line_length;
+            src += ds_get_linesize(ds);
+        }
+        break;
+    default:
+        fprintf(stderr, "%s:%d: Oops: unsupported depth %d\n",
+                __FUNCTION__, __LINE__, depth);
+        exit(1);
+    }
+}
+
+/* -------------------------------------------------------------------- */
+/* 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) {
+        resize_screen = 0;
+        fbdev_cls();
+        cx = 0; cy = 0;
+        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;
+        }
+    }
+    switch (fb_var.bits_per_pixel) {
+    case 32:
+        fbdev_render_32(ds, x, y, w, h);
+        break;
+    }
+}
+
+static void fbdev_resize(DisplayState *ds)
+{
+    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++;
+    case FB_ACTIVE:
+        break;
+    }
+
+    vga_hw_update();
+
+    if (redraw_screen) {
+        redraw_screen = 0;
+        fbdev_cls();
+        fbdev_update(ds, 0, 0, ds_get_width(ds), ds_get_height(ds));
+    }
+}
+
+void fbdev_display_init(DisplayState *ds, const char *device)
+{
+    if (fbdev_init(device) != 0) {
+        exit(1);
+    }
+    atexit(fbdev_cleanup);
+    fbdev_switch_init();
+    fbdev_catch_exit_signals();
+
+    dcl = qemu_mallocz(sizeof(DisplayChangeListener));
+    dcl->dpy_update  = fbdev_update;
+    dcl->dpy_resize  = fbdev_resize;
+    dcl->dpy_refresh = fbdev_refresh;
+    register_displaychangelistener(ds, dcl);
+}
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/qemu-options.hx b/qemu-options.hx
index 12f6b51..c4ffe8e 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -663,6 +663,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 fa921df..4a0c81b 100644
--- a/sysemu.h
+++ b/sysemu.h
@@ -98,6 +98,7 @@  typedef enum DisplayType
     DT_CURSES,
     DT_SDL,
     DT_VNC,
+    DT_FBDEV,
     DT_NOGRAPHIC,
 } DisplayType;
 
diff --git a/vl.c b/vl.c
index 85bcc84..6ecebe7 100644
--- a/vl.c
+++ b/vl.c
@@ -3216,6 +3216,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;
@@ -3781,6 +3786,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);