/* framebuf.c
 * Linux framebuffer code
 * (c) 2002 Petr 'Brain' Kulhavy
 * This file is a part of the Links program, released under GPL.
 */

#include "cfg.h"

#ifdef GRDRV_FB

/*#define USE_FB_ACCEL*/
/*#define USE_FB_ACCEL_FILLRECT*/

/* #define FB_DEBUG */
/* #define SC_DEBUG */

/* note: SIGUSR1 is used by libpthread and is disabled even if no thread
   functions are called --- do not use */

#define SIG_REL	SIGUSR2
#define SIG_ACQ	SIGVTALRM

#if defined(FB_DEBUG) || defined(SC_DEBUG)
#define MESSAGE(a) fprintf(stderr,"%s",a);
#endif

#include "links.h"

#include "bits.h"

#include <gpm.h>

#include <sys/mman.h>
#include <sys/ioctl.h>

#include <linux/fb.h>
#include <linux/kd.h>
#include <linux/vt.h>

#include "arrow.inc"

#ifndef MAP_FAILED
#define MAP_FAILED	((void *)-1L)
#endif

#ifdef GPM_HAVE_SMOOTH
#define gpm_smooth GPM_SMOOTH
#else
#define gpm_smooth 0
#endif

#if defined(USE_FB_ACCEL)
#if !defined(FBIO_ACCEL_SUPPORT)
#define FBIO_ACCEL_SUPPORT		0x4630
#define FBIO_ACCEL_SYNC			0x4631
#define FBIO_ACCEL_FILLRECT		0x4632
#define FBIO_ACCEL_FILLRECT_SYNC	0x4633
#define FBIO_ACCEL_COPYAREA		0x4634
#define FBIO_ACCEL_COPYAREA_SYNC	0x4635
#define FB_ACCEL_FILLRECT_SUPPORTED	0x00000001
#define FB_ACCEL_FILLRECT_ACCELERATED	0x00000002
#define FB_ACCEL_COPYAREA_SUPPORTED	0x00000004
#define FB_ACCEL_COPYAREA_ACCELERATED	0x00000008
#define FB_ACCEL_SYNC_NEEDED		0x40000000
/* we don't know if fb_copyarea and fb_fillrect are or aren't defined */
#define fb_copyarea fb_redefined_copyarea
#define fb_fillrect fb_redefined_fillrect
struct fb_copyarea {
	__u32 dx;
	__u32 dy;
	__u32 width;
	__u32 height;
	__u32 sx;
	__u32 sy;
};
struct fb_fillrect {
	__u32 dx;
	__u32 dy;
	__u32 width;
	__u32 height;
	__u32 color;
	__u32 rop;
};
#endif
#ifndef ROP_COPY
#define ROP_COPY	0
#endif
#ifndef ROP_XOR
#define ROP_XOR		1
#endif
#endif

static int TTY = 0;

static int fb_hgpm = -2;
static int fb_hmice = -1;
static unsigned char ps2_mouse_mode;

#define fb_have_mouse	(fb_hgpm >= 0 || fb_hmice >= 0)

static int fb_console;

static struct itrm *fb_kbd;

static struct graphics_device *fb_old_vd;
static struct graphics_device *fb_block_dev;

static int fb_handle = -1;
static unsigned char *fb_mem, *fb_vmem;
static unsigned fb_mem_size;
static unsigned fb_mapped_size;
static int fb_linesize, fb_bits_pp, fb_pixelsize;
static int fb_xsize, fb_ysize;
static int border_left, border_right, border_top, border_bottom;
static int fb_colors, fb_palette_colors;
static struct fb_var_screeninfo vi;
static struct fb_fix_screeninfo fi;

static void fb_draw_bitmap(struct graphics_device *dev, struct bitmap *bmp, int x, int y);

static unsigned char *fb_driver_param;
extern struct graphics_driver fb_driver;
static int have_cmap;
static volatile int fb_active;

struct palette {
	unsigned short *red;
	unsigned short *green;
	unsigned short *blue;
};

static struct palette old_palette;
static struct palette global_pal;
static struct vt_mode vt_mode, vt_omode;

/*static struct fb_var_screeninfo oldmode;*/

static volatile int in_gr_operation;

#ifdef USE_FB_ACCEL
static int accel_flags;
static int need_accel_sync;
static inline void accel_sync(void)
{
	int rs;
	if (need_accel_sync) {
		EINTRLOOP(rs, ioctl(fb_handle, FBIO_ACCEL_SYNC));
		need_accel_sync = 0;
	}
}
#else
#define accel_sync()	do { } while (0)
#endif

/* mouse */
static int mouse_x, mouse_y;		/* mouse pointer coordinates */
static long mouse_black, mouse_white;
static int background_x, background_y; /* Where was the mouse background taken from */
static unsigned char *mouse_buffer, *background_buffer, *new_background_buffer;
static struct graphics_device *mouse_graphics_device;
static int global_mouse_hidden;
static int last_mouse_buttons;


#define TEST_MOUSE(xl,xh,yl,yh) if (RECTANGLES_INTERSECT(\
					(xl),(xh),\
					background_x,background_x+arrow_width,\
					(yl),(yh),\
					background_y,background_y+arrow_height)\
					&& !global_mouse_hidden){\
					mouse_hidden=1;\
					hide_mouse();\
				}else mouse_hidden=0;

#define END_MOUSE if (mouse_hidden) show_mouse();

#define END_GR	\
		memory_barrier(&fb_vmem); \
		in_gr_operation--;\
		if (!fb_active && !in_gr_operation) {\
			accel_sync();\
			EINTRLOOP(rs, ioctl(TTY,VT_RELDISP,1));\
		}
#define INC_IN_GR \
		in_gr_operation++; \
		memory_barrier(&fb_vmem);
#define START_GR \
		INC_IN_GR \
		if (!fb_active) { END_GR return; }
#define START_GR_0 \
		INC_IN_GR \
		if (!fb_active) { END_GR return 0; }


#define NUMBER_OF_DEVICES	10

#define TEST_INACTIVITY if (!fb_active||dev!=current_virtual_device) return;

#define TEST_INACTIVITY_0 if (!fb_active||dev!=current_virtual_device) return 0;

#define RECTANGLES_INTERSECT(xl0, xh0, xl1, xh1, yl0, yh0, yl1, yh1) (\
				   (xl0)<(xh1)\
				&& (xl1)<(xh0)\
				&& (yl0)<(yh1)\
				&& (yl1)<(yh0))

/* This assures that x, y, xs, ys, data will be sane according to clipping
 * rectangle. If nothing lies within this rectangle, the current function
 * returns. The data pointer is automatically advanced by this macro to reflect
 * the right position to start with inside the bitmap. */
#define	CLIP_PREFACE \
	int mouse_hidden;\
	int xs, ys;\
	unsigned char *data=bmp->data;\
\
	if (!data)\
		return;\
\
	TEST_INACTIVITY\
	CLIP_DRAW_BITMAP\
	xs = bmp->x;\
	ys = bmp->y;\
	if (x+xs>dev->clip.x2) xs=dev->clip.x2-x;\
	if (y+ys>dev->clip.y2) ys=dev->clip.y2-y;\
	if (dev->clip.x1-x>0){\
		xs-=(dev->clip.x1-x);\
		data+=fb_pixelsize*(dev->clip.x1-x);\
		x=dev->clip.x1;\
	}\
	if (dev->clip.y1-y>0){\
		ys-=(dev->clip.y1-y);\
		data+=bmp->skip*(dev->clip.y1-y);\
		y=dev->clip.y1;\
	}\
	/* xs, ys: how much pixels to paint\
	 * data: where to start painting from\
	 */\
	START_GR\
	TEST_MOUSE(x,x+xs,y,y+ys)


/* fill_area: 5,5,10,10 fills in 25 pixels! */

/* This assures that x1, x2, y1, y2 will be sane according to the
 * clipping rectangle set up by set_clip_area. If empty region
 * results, return from current function occurs. */
#define FILL_CLIP_PREFACE \
	int mouse_hidden;\
	TEST_INACTIVITY\
	CLIP_FILL_AREA\
	START_GR\
	TEST_MOUSE(x1,x2,y1,y2)


#define HLINE_CLIP_PREFACE \
	int mouse_hidden;\
	TEST_INACTIVITY\
	CLIP_DRAW_HLINE\
	START_GR\
	TEST_MOUSE(x1,x2,y,y+1)

#define VLINE_CLIP_PREFACE \
	int mouse_hidden;\
	TEST_INACTIVITY\
	CLIP_DRAW_VLINE\
	START_GR\
	TEST_MOUSE(x,x+1,y1,y2)

#define SCROLL_CLIP_PREFACE \
	int mouse_hidden;\
	TEST_INACTIVITY_0\
	START_GR_0\
	TEST_MOUSE(dev->clip.x1, dev->clip.x2, dev->clip.y1, dev->clip.y2)

#include "fbcommon.inc"

static void redraw_mouse(void);

static void fb_mouse_move(int dx, int dy)
{
	struct links_event ev;

	if (!fb_have_mouse)
		return;

	mouse_x += dx;
	mouse_y += dy;
	ev.ev = EV_MOUSE;
	if (mouse_x >= fb_xsize) mouse_x = fb_xsize - 1;
	if (mouse_y >= fb_ysize) mouse_y = fb_ysize - 1;
	if (mouse_x < 0) mouse_x = 0;
	if (mouse_y < 0) mouse_y = 0;
	ev.x = mouse_x;
	ev.y = mouse_y;
	ev.b = B_MOVE;
	if (((last_mouse_buttons & BM_ACT) == B_DOWN || (last_mouse_buttons & BM_ACT) == B_DRAG) &&
	    !BM_IS_WHEEL(last_mouse_buttons)) {
		ev.b = (last_mouse_buttons & BM_BUTT) | B_DRAG;
	}
	if (!current_virtual_device) return;
	if (current_virtual_device->mouse_handler) current_virtual_device->mouse_handler(current_virtual_device, ev.x, ev.y, (int)ev.b);
	redraw_mouse();
}

static void fb_key_in(struct itrm *p, unsigned char *ev_, int size)
{
	int rs;
	struct links_event *ev = (struct links_event *)(void *)ev_;
	if (size != sizeof(struct links_event)) return;
	if (ev->ev == EV_ABORT) terminate_loop = 1;
	if (ev->ev != EV_KBD) return;
	if (ev->y & KBD_PASTING) goto skip;
	if ((ev->y & (KBD_CTRL | KBD_ALT)) == KBD_ALT && ev->x >= '0' && ev->x <= '9') {
		switch_virtual_device((ev->x - '1' + 10) % 10);
		goto flush_ret;
	}
	if (!ev->y && ev->x == KBD_F5) fb_mouse_move(-3, 0);
	else if (!ev->y && ev->x == KBD_F6) fb_mouse_move(0, 3);
	else if (!ev->y && ev->x == KBD_F7) fb_mouse_move(0, -3);
	else if (!ev->y && ev->x == KBD_F8) fb_mouse_move(3, 0);
	else {
skip:
		if (g_kbd_codepage(&fb_driver) != utf8_table && ev->x >= 128 && ev->x <= 255)
			if ((ev->x = cp2u(ev->x, g_kbd_codepage(&fb_driver))) == -1) return;
		if (current_virtual_device && current_virtual_device->keyboard_handler) current_virtual_device->keyboard_handler(current_virtual_device, ev->x, ev->y);
	}
flush_ret:
	EINTRLOOP(rs, fsync(fb_handle));
}




#define mouse_getscansegment(buf,x,y,w) memcpy_to_fb(buf, fb_vmem + y * fb_linesize + x * fb_pixelsize, w, 1)
#define mouse_drawscansegment(ptr,x,y,w) memcpy_to_fb(fb_vmem + y * fb_linesize + x * fb_pixelsize, ptr, w, 0)

#define do_with_mouse_device(cmd)				\
do {								\
	struct graphics_device *current_virtual_device_backup;	\
								\
	current_virtual_device_backup = current_virtual_device;	\
	current_virtual_device = mouse_graphics_device;		\
	cmd;							\
	current_virtual_device = current_virtual_device_backup;	\
} while (0)

/* Flushes the background_buffer onscreen where it was originally taken from. */
static void place_mouse_background(void)
{
	struct bitmap bmp;

	bmp.x=arrow_width;
	bmp.y=arrow_height;
	bmp.skip=arrow_width*fb_pixelsize;
	bmp.data=background_buffer;

	do_with_mouse_device(fb_draw_bitmap(mouse_graphics_device, &bmp, background_x, background_y));

}

/* Only when the old and new mouse don't interfere. Using it on interfering mouses would
 * cause a flicker.
 */
static void hide_mouse(void)
{
	global_mouse_hidden=1;
	place_mouse_background();
}

/* Gets background from the screen (clipping provided only right and bottom) to the
 * passed buffer.
 */
static void get_mouse_background(unsigned char *buffer_ptr)
{
	int width,height,skip,x,y;

	skip=arrow_width*fb_pixelsize;

	x=mouse_x;
	y=mouse_y;

	width=fb_pixelsize*(arrow_width+x>fb_xsize?fb_xsize-x:arrow_width);
	height=arrow_height+y>fb_ysize?fb_ysize-y:arrow_height;

	accel_sync();

	for (;height;height--){
		mouse_getscansegment(buffer_ptr,x,y,width);
		buffer_ptr+=skip;
		y++;
	}
}

/* Overlays the arrow's image over the mouse_buffer
 * Doesn't draw anything into the screen
 */
static void render_mouse_arrow(void)
{
	int x,y;
	unsigned reg0, reg1;
	unsigned char *mouse_ptr=mouse_buffer;
	const unsigned *arrow_ptr=arrow;

	for (y=arrow_height;y;y--){
		reg0=*arrow_ptr;
		reg1=arrow_ptr[1];
		arrow_ptr+=2;
		for (x=arrow_width;x;)
		{
			unsigned mask=1U<<(--x);

			if (reg0&mask)
				memcpy (mouse_ptr, &mouse_black, fb_pixelsize);
			else if (reg1&mask)
				memcpy (mouse_ptr, &mouse_white, fb_pixelsize);
			mouse_ptr+=fb_pixelsize;
		}
	}
}

static void place_mouse(void)
{
	struct bitmap bmp;

	if (!fb_have_mouse)
		return;

	bmp.x=arrow_width;
	bmp.y=arrow_height;
	bmp.skip=arrow_width*fb_pixelsize;
	bmp.data=mouse_buffer;
	do_with_mouse_device(fb_draw_bitmap(mouse_graphics_device, &bmp, mouse_x, mouse_y));
	global_mouse_hidden=0;
}

/* Only when the old and the new mouse positions do not interfere. Using this routine
 * on interfering positions would cause a flicker.
 */
static void show_mouse(void)
{
	get_mouse_background(background_buffer);
	background_x=mouse_x;
	background_y=mouse_y;
	memcpy(mouse_buffer,background_buffer,fb_pixelsize*arrow_area);
	render_mouse_arrow();
	place_mouse();
}

/* Doesn't draw anything into the screen
 */
static void put_and_clip_background_buffer_over_mouse_buffer(void)
{
	unsigned char *bbufptr=background_buffer, *mbufptr=mouse_buffer;
	int left=background_x-mouse_x;
	int top=background_y-mouse_y;
	int right,bottom;
	int bmpixelsizeL=fb_pixelsize;
	int number_of_bytes;
	int byte_skip;

	right=left+arrow_width;
	bottom=top+arrow_height;

	if (left<0){
		bbufptr-=left*bmpixelsizeL;
		left=0;
	}
	if (right>arrow_width) right=arrow_width;
	if (top<0){
		bbufptr-=top*bmpixelsizeL*arrow_width;
		top=0;
	}
	if (bottom>arrow_height) bottom=arrow_height;
	mbufptr+=bmpixelsizeL*(left+arrow_width*top);
	byte_skip=arrow_width*bmpixelsizeL;
	number_of_bytes=bmpixelsizeL*(right-left);
	for (;top<bottom;top++){
		memcpy(mbufptr,bbufptr,number_of_bytes);
		mbufptr+=byte_skip;
		bbufptr+=byte_skip;
	}
}

/* This draws both the contents of background_buffer and mouse_buffer in a scan
 * way (left-right, top-bottom), so the flicker is reduced.
 */
static inline void place_mouse_composite(void)
{
	int mouse_left=mouse_x;
	int mouse_top=mouse_y;
	int background_left=background_x;
	int background_top=background_y;
	int mouse_right=mouse_left+arrow_width;
	int mouse_bottom=mouse_top+arrow_height;
	int background_right=background_left+arrow_width;
	int background_bottom=background_top+arrow_height;
	int skip=arrow_width*fb_pixelsize;
	int background_length,mouse_length;
	unsigned char *mouse_ptr=mouse_buffer,*background_ptr=background_buffer;
	int yend;

	if (mouse_bottom>fb_ysize) mouse_bottom=fb_ysize;
	if (background_bottom>fb_ysize) background_bottom=fb_ysize;

	accel_sync();

	/* Let's do the top part */
	if (background_top<mouse_top){
		/* Draw the background */
		background_length=background_right>fb_xsize?fb_xsize-background_left
			:arrow_width;
		for (;background_top<mouse_top;background_top++){
			mouse_drawscansegment(background_ptr,background_left
				,background_top,background_length*fb_pixelsize);
			background_ptr+=skip;
		}

	}else if (background_top>mouse_top){
		/* Draw the mouse */
		mouse_length=mouse_right>fb_xsize
			?fb_xsize-mouse_left:arrow_width;
		for (;mouse_top<background_top;mouse_top++){
			mouse_drawscansegment(mouse_ptr,mouse_left,mouse_top,mouse_length*fb_pixelsize);
			mouse_ptr+=skip;
		}
	}

	/* Let's do the middle part */
	yend=mouse_bottom<background_bottom?mouse_bottom:background_bottom;
	if (background_left<mouse_left){
		/* Draw background, mouse */
		mouse_length=mouse_right>fb_xsize?fb_xsize-mouse_left:arrow_width;
		for (;mouse_top<yend;mouse_top++){
			mouse_drawscansegment(background_ptr,background_left,mouse_top
				,(mouse_left-background_left)*fb_pixelsize);
			mouse_drawscansegment(mouse_ptr,mouse_left,mouse_top,mouse_length*fb_pixelsize);
			mouse_ptr+=skip;
			background_ptr+=skip;
		}

	}else{
		int l1, l2, l3;

		/* Draw mouse, background */
		mouse_length=mouse_right>fb_xsize?fb_xsize-mouse_left:arrow_width;
		background_length=background_right-mouse_right;
		if (background_length+mouse_right>fb_xsize)
			background_length=fb_xsize-mouse_right;
		l1=mouse_length*fb_pixelsize;
		l2=(mouse_right-background_left)*fb_pixelsize;
		l3=background_length*fb_pixelsize;
		for (;mouse_top<yend;mouse_top++){
			mouse_drawscansegment(mouse_ptr,mouse_left,mouse_top,l1);
			if (background_length>0)
				mouse_drawscansegment(
					background_ptr + l2,
					mouse_right,mouse_top, l3);
			mouse_ptr+=skip;
			background_ptr+=skip;
		}
	}

	if (background_bottom<mouse_bottom){
		/* Count over bottoms! tops will be invalid! */
		/* Draw mouse */
		mouse_length=mouse_right>fb_xsize?fb_xsize-mouse_left
			:arrow_width;
		for (;background_bottom<mouse_bottom;background_bottom++){
			mouse_drawscansegment(mouse_ptr,mouse_left,background_bottom
				,mouse_length*fb_pixelsize);
			mouse_ptr+=skip;
		}
	}else{
		/* Draw background */
		background_length=background_right>fb_xsize?fb_xsize-background_left
			:arrow_width;
		for (;mouse_bottom<background_bottom;mouse_bottom++){
			mouse_drawscansegment(background_ptr,background_left,mouse_bottom
				,background_length*fb_pixelsize);
			background_ptr+=skip;
		}
	}
}

/* This moves the mouse a sophisticated way when the old and new position of the
 * cursor overlap.
 */
static inline void redraw_mouse_sophisticated(void)
{
	int new_background_x, new_background_y;

	get_mouse_background(mouse_buffer);
	put_and_clip_background_buffer_over_mouse_buffer();
	memcpy(new_background_buffer,mouse_buffer,fb_pixelsize*arrow_area);
	new_background_x=mouse_x;
	new_background_y=mouse_y;
	render_mouse_arrow();
	place_mouse_composite();
	memcpy(background_buffer,new_background_buffer,fb_pixelsize*arrow_area);
	background_x=new_background_x;
	background_y=new_background_y;
}

static void redraw_mouse(void)
{
	int rs;
	START_GR;
	if (mouse_x!=background_x||mouse_y!=background_y){
		if (RECTANGLES_INTERSECT(
			background_x, background_x+arrow_width,
			mouse_x, mouse_x+arrow_width,
			background_y, background_y+arrow_height,
			mouse_y, mouse_y+arrow_height)){
			redraw_mouse_sophisticated();
		}else{
			/* Do a normal hide/show */
			get_mouse_background(mouse_buffer);
			memcpy(new_background_buffer,
				mouse_buffer,arrow_area*fb_pixelsize);
			render_mouse_arrow();
			hide_mouse();
			place_mouse();
			memcpy(background_buffer,new_background_buffer
				,arrow_area*fb_pixelsize);
			background_x=mouse_x;
			background_y=mouse_y;
		}
	}
	END_GR;
}

static void alloc_palette(struct palette *pal)
{
	pal->red = mem_calloc(sizeof(unsigned short) * fb_palette_colors);
	pal->green = mem_calloc(sizeof(unsigned short) * fb_palette_colors);
	pal->blue = mem_calloc(sizeof(unsigned short) * fb_palette_colors);
}

static void free_palette(struct palette *pal)
{
	if (pal->red) mem_free(pal->red);
	if (pal->green) mem_free(pal->green);
	if (pal->blue) mem_free(pal->blue);
}

/* This is an empiric magic that ensures
 * Good white purity
 * Correct rounding and dithering prediction
 * And this is the cabbala:
 * 063 021 063
 * 009 009 021
 * 255 085 255
 * 036 036 084
 */
static void generate_palette(struct palette *pal)
{
	int a;

	alloc_palette(pal);

	switch (fb_colors) {
		case 16:
		case 256:
			for (a = 0; a < fb_palette_colors; a++) {
				unsigned rgb[3];
				int cols = fb_colors;
				if (cols == 256 && (fb_driver.depth & 0x300) == 0x300)
					cols = 216;
				q_palette(cols, a, 65535, rgb);
				pal->red[a] = rgb[0];
				pal->green[a] = rgb[1];
				pal->blue[a] = rgb[2];
			}
			break;
		case 32768:
			for (a = 0; a < fb_palette_colors; a++) {
				/*
				pal->red[a] = ((a >> 10) & 31) * (65535 / 31);
				pal->green[a] = ((a >> 5) & 31) * (65535 / 31);
				pal->blue[a] = (a & 31) * (65535 / 31);
				 */
				pal->red[a] =
				pal->green[a] =
				pal->blue[a] = (((a & 31) * 255) / 31) * 257;
			}
			break;
		case 65536:
			for (a = 0; a < fb_palette_colors; a++){
				 /*
				pal->red[a] = ((a >> 11) & 31) * (65535 / 31);
				pal->green[a] = ((a >> 5) & 63) * (65535 / 63);
				pal->blue[a] = (a & 31) * (65535 / 31);
				 */
				pal->green[a] = (((a & 63) * 255) / 64) * 257;
				pal->red[a] =
				pal->blue[a] = (((a & 31) * 255) / 32) * 257;
			}
			break;
		default:
			for (a = 0; a < fb_palette_colors; a++) {
				pal->red[a] =
				pal->green[a] =
				pal->blue[a] = a * 257;
				/* stuff it in both high and low byte */
			}
	}
}

static void set_palette(struct palette *pal)
{
	struct fb_cmap cmap;
	int i;
	unsigned short *red = pal->red;
	unsigned short *green = pal->green;
	unsigned short *blue = pal->blue;
	__u16 *r, *g, *b, *t;
	int rs;

	if (!red || !green || !blue)
		return;

	r = mem_alloc(fb_palette_colors * sizeof(__u16));
	g = mem_alloc(fb_palette_colors * sizeof(__u16));
	b = mem_alloc(fb_palette_colors * sizeof(__u16));
	t = mem_calloc(fb_palette_colors * sizeof(__u16));

	for (i = 0; i < fb_palette_colors; i++) {
		r[i] = red[i];
		g[i] = green[i];
		b[i] = blue[i];
		/*fprintf(stderr, "%d %d %d\n", r[i], g[i], b[i]);*/
		/*fprintf(stderr, "%5x: %5x\t%5x\t%5x\t%5x\n",i,r[i],g[i],b[i],t[i]);*/

	}

	cmap.start = 0;
	cmap.len = fb_palette_colors;
	cmap.red = r;
	cmap.green = g;
	cmap.blue = b;
	cmap.transp = t;

	EINTRLOOP(rs, ioctl(fb_handle, FBIOPUTCMAP, &cmap));
	if (rs == -1) {
		/*error("Cannot set palette\n")*/;
	}

	mem_free(r);
	mem_free(g);
	mem_free(b);
	mem_free(t);
}

static void get_palette(struct palette *pal)
{
	struct fb_cmap cmap;
	int i;
	__u16 *r, *g, *b, *t;
	int rs;

	pal->red = pal->green = pal->blue = NULL;

	r = mem_alloc(fb_palette_colors * sizeof(__u16));
	g = mem_alloc(fb_palette_colors * sizeof(__u16));
	b = mem_alloc(fb_palette_colors * sizeof(__u16));
	t = mem_alloc(fb_palette_colors * sizeof(__u16));

	cmap.start = 0;
	cmap.len = fb_palette_colors;
	cmap.red = r;
	cmap.green = g;
	cmap.blue = b;
	cmap.transp = t;

	EINTRLOOP(rs, ioctl(fb_handle, FBIOGETCMAP, &cmap));
	if (rs == -1) {
		/*error("Cannot get palette\n")*/;
		goto skip;
	}

	alloc_palette(pal);

	for (i = 0; i < fb_palette_colors; i++) {
		/*printf("%d %d %d\n",r[i],g[i],b[i]);*/
		pal->red[i] = r[i];
		pal->green[i] = g[i];
		pal->blue[i] = b[i];
	}

skip:
	mem_free(r);
	mem_free(g);
	mem_free(b);
	mem_free(t);
}

static void fb_clear_videoram(void)
{
	int rs;
	START_GR
#ifdef USE_FB_ACCEL_FILLRECT
	if (accel_flags & FB_ACCEL_FILLRECT_ACCELERATED) {
		struct fb_fillrect f;
		f.dx = 0;
		f.dy = 0;
		f.width = fb_xsize + border_left + border_right;
		f.height = fb_ysize + border_top + border_bottom;
		f.color = 0;
		f.rop = ROP_COPY;
		EINTRLOOP(rs, ioctl(fb_handle, FBIO_ACCEL_FILLRECT_SYNC, &f));
		if (rs < 0) {
			error("fb_clear_videoram accel failed\n");
			goto no_accel;
		}
	} else
	no_accel:
#endif
	{
		accel_sync();
		memset(fb_mem, 0, (border_top + fb_ysize + border_bottom) * fb_linesize);
		/*{
			int size = (border_top + fb_ysize + border_bottom) * fb_linesize;
			int pos;
			for (pos = 0; pos < size; pos++) {
				fb_mem[pos] = (unsigned char)pos;
			}
		}*/
	}
	END_GR
}

static void fb_switch_signal_rel(void *data)
{
	int rs;
	fb_active = 0;
	if (!in_gr_operation) {
		accel_sync();
		EINTRLOOP(rs, ioctl(TTY, VT_RELDISP, 1));
	}
}

static void fb_switch_signal_acq(void *data)
{
	struct vt_stat st;
	int rs;

	EINTRLOOP(rs, ioctl(TTY, VT_GETSTATE, &st));
	if (rs) return;
	if (st.v_active != fb_console) return;
	INC_IN_GR
	fb_active = 1;
	EINTRLOOP(rs, ioctl(TTY, VT_RELDISP, VT_ACKACQ));
	/*
	 * There is a race condition in Linux NVidia framebuffer driver
	 * It still draws into a framebuffer here, so we have to sleep
	 */
	portable_sleep(10);
	if (have_cmap && current_virtual_device)
		set_palette(&global_pal);
	END_GR
	if (border_left | border_top | border_right | border_bottom) fb_clear_videoram();
	if (current_virtual_device) current_virtual_device->redraw_handler(current_virtual_device, &current_virtual_device->size);
	EINTRLOOP(rs, fsync(fb_handle));
}


static unsigned char *fb_switch_init(void)
{
	int rs;

	INC_IN_GR

	/* If we use threads, the signal handler may execute on a different
	   thread. framebuf.c doesn't handle different-thread signals, so
	   we must switch to synchronous signal handling when using threads */
	install_signal_handler(SIG_REL, fb_switch_signal_rel, NULL,
#ifndef EXEC_IN_THREADS
		1
#else
		0
#endif
		);
	install_signal_handler(SIG_ACQ, fb_switch_signal_acq, NULL, 0);
	EINTRLOOP(rs, ioctl(TTY, VT_GETMODE, &vt_omode));
	if (rs == -1) {
		in_gr_operation--;
		return stracpy(cast_uchar "Could not get VT mode.\n");
	}
	memcpy(&vt_mode, &vt_omode, sizeof(vt_mode));

	vt_mode.mode = VT_PROCESS;
	vt_mode.waitv = 0;
	vt_mode.relsig = SIG_REL;
	vt_mode.acqsig = SIG_ACQ;

	EINTRLOOP(rs, ioctl(TTY, VT_SETMODE, &vt_mode));
	if (rs == -1) {
		in_gr_operation--;
		return stracpy(cast_uchar "Could not set VT mode.\n");
	}

	EINTRLOOP(rs, ioctl(TTY, VT_WAITACTIVE, fb_console));
	fb_active = 1;

	return NULL;
}

static void fb_switch_shutdown(void)
{
	int rs;
	if (in_gr_operation <= 0) internal_error("fb_switch_shutdown: in_gr_operation %d", in_gr_operation);
	if (!fb_active && in_gr_operation == 1) {
		EINTRLOOP(rs, ioctl(TTY, VT_RELDISP, 1));
	}
	EINTRLOOP(rs, ioctl(TTY, VT_SETMODE, &vt_omode));
	install_signal_handler(SIG_REL, (void (*)(void *))NULL, (void*)SIG_REL, 1);
	install_signal_handler(SIG_ACQ, (void (*)(void *))NULL, (void*)SIG_ACQ, 0);
	in_gr_operation--;
}

static void fb_shutdown_palette(void)
{
	if (have_cmap) {
		if (fb_active)
			set_palette(&old_palette);
		free_palette(&old_palette);
		free_palette(&global_pal);
	}
}

static void fb_ctrl_c(void *i_)
{
	kbd_ctrl_c();
}

static void unhandle_fb_mouse(void);

static void fb_gpm_in(void *nic)
{
	struct links_event ev;
	int g;
	Gpm_Event gev;
	int rs;
	again:
	set_handlers(fb_hgpm, (void (*)(void *))NULL, (void (*)(void *))NULL, NULL);
	save_gpm_signals();
	g = Gpm_GetEvent(&gev);
	restore_gpm_signals();
	if (g <= 0) {
		fb_hgpm = -1;
		hide_mouse();
		unhandle_fb_mouse();
		return;
	}
	set_handlers(fb_hgpm, fb_gpm_in, (void (*)(void *))NULL, NULL);
	if (fb_hmice != -1)
		return;
	/*fprintf(stderr, "%x %x %d %d %d %d\n", gev.type, gev.buttons, gev.dx, gev.dy, gev.wdx, gev.wdy);*/
	if (gev.dx || gev.dy) {
		if (!((int)gev.type & gpm_smooth)) {
			mouse_x += gev.dx * 8;
			mouse_y += gev.dy * 8;
		}
#ifdef GPM_HAVE_SMOOTH
		else {
			mouse_x += gev.dx;
			mouse_y += gev.dy;
		}
#endif
	}
	ev.ev = EV_MOUSE;
	if (mouse_x >= fb_xsize) mouse_x = fb_xsize - 1;
	if (mouse_y >= fb_ysize) mouse_y = fb_ysize - 1;
	if (mouse_x < 0) mouse_x = 0;
	if (mouse_y < 0) mouse_y = 0;

	if (!((int)gev.type & gpm_smooth) && (gev.dx || gev.dy)) {
		mouse_x = (mouse_x + 8) / 8 * 8 - 4;
		mouse_y = (mouse_y + 8) / 8 * 8 - 4;
		if (mouse_x >= fb_xsize) mouse_x = fb_xsize - 1;
		if (mouse_y >= fb_ysize) mouse_y = fb_ysize - 1;
		if (mouse_x < 0) mouse_x = 0;
		if (mouse_y < 0) mouse_y = 0;
	}

	ev.x = mouse_x;
	ev.y = mouse_y;
	if (gev.buttons & GPM_B_LEFT) ev.b = B_LEFT;
	else if (gev.buttons & GPM_B_MIDDLE) ev.b = B_MIDDLE;
	else if (gev.buttons & GPM_B_RIGHT) ev.b = B_RIGHT;
#ifdef GPM_B_FOURTH
	else if (gev.buttons & GPM_B_FOURTH) ev.b = B_FOURTH;
#endif
#ifdef GPM_B_UP
	else if (gev.buttons & GPM_B_UP) ev.b = B_FIFTH;
#endif
#ifdef GPM_B_DOWN
	else if (gev.buttons & GPM_B_DOWN) ev.b = B_SIXTH;
#endif
	else ev.b = 0;
	if ((int)gev.type & GPM_DOWN) ev.b |= B_DOWN;
	else if ((int)gev.type & GPM_UP) ev.b |= B_UP;
	else if ((int)gev.type & GPM_DRAG) ev.b |= B_DRAG;
	else ev.b |= B_MOVE;

#ifdef HAVE_WDX_WDY
	if ((ev.b & BM_ACT) == B_DRAG || (ev.b & BM_ACT) == B_MOVE) {
		if (gev.wdy < 0) {
			ev.b &= ~BM_BUTT;
			ev.b |= B_WHEELDOWN;
		} else if (gev.wdy > 0) {
			ev.b &= ~BM_BUTT;
			ev.b |= B_WHEELUP;
		}
#if 0
	/* it doesn't work anyway - the exps2 protocol doesn't support it and evdev support in gpm is buggy */
		else if (gev.wdx < 0) {
			ev.b &= ~BM_BUTT;
			ev.b |= B_WHEELRIGHT;
		} else if (gev.wdx > 0) {
			ev.b &= ~BM_BUTT;
			ev.b |= B_WHEELLEFT;
#endif
	}
#endif

	if (((ev.b & BM_ACT) == B_MOVE && !(ev.b & BM_BUTT)) || (ev.b & BM_ACT) == B_DRAG) {
		if (can_read(fb_hgpm)) goto again;
	}

	last_mouse_buttons = (int)ev.b;
	if (!current_virtual_device) return;
	if (current_virtual_device->mouse_handler) current_virtual_device->mouse_handler(current_virtual_device, ev.x, ev.y, (int)ev.b);
	redraw_mouse();
	EINTRLOOP(rs, fsync(fb_handle));
}

static void fb_ps2_in(void *nic)
{
	unsigned char buffer[4];

	int rs, dx, dy, dw, button;

	int size = ps2_mouse_mode ? 4 : 3;
	if (hard_read(fb_hmice, buffer, size) != size) {
		unhandle_fb_mouse();
		return;
	}

	dx = buffer[1];
	if (buffer[0] & 0x10) dx |= -0x100;
	dy = buffer[2];
	if (buffer[0] & 0x20) dy |= -0x100;
	dy = -dy;

	button = -1;
	if (buffer[0] & 1) button = B_LEFT;
	else if (buffer[0] & 2) button = B_RIGHT;
	else if (buffer[0] & 4) button = B_MIDDLE;
	else if (ps2_mouse_mode == 4) {
		if (buffer[3] & 0x10) button = B_FOURTH;
		else if (buffer[3] & 0x20) button = B_FIFTH;
	}

	dw = 0;
	if (ps2_mouse_mode == 3) {
		dw = buffer[3];
		if (dw & 0x80) dw |= -0x100;
	}
	if (ps2_mouse_mode == 4) {
		dw = buffer[3] & 0x0f;
		if (dw & 0x08) dw |= -0x10;
	}

	if (fb_active) {
		mouse_x += dx;
		mouse_y += dy;
	}

	if (mouse_x >= fb_xsize) mouse_x = fb_xsize - 1;
	if (mouse_y >= fb_ysize) mouse_y = fb_ysize - 1;
	if (mouse_x < 0) mouse_x = 0;
	if (mouse_y < 0) mouse_y = 0;

	if ((last_mouse_buttons & BM_ACT) == B_UP || (last_mouse_buttons & BM_ACT) == B_MOVE) {
		if (button == -1)
			button = B_MOVE;
		else
			button |= B_DOWN;
	} else {
		if (button == -1)
			button = (last_mouse_buttons & BM_BUTT) | B_UP;
		else
			button |= B_DRAG;
	}

	if ((button & BM_ACT) == B_DRAG || (button & BM_ACT) == B_MOVE) {
		if (dw > 0) {
			button &= ~BM_BUTT;
			button |= B_WHEELDOWN;
		} else if (dw < 0) {
			button &= ~BM_BUTT;
			button |= B_WHEELUP;
		}
	}

	/*fprintf(stderr, "%02x %d %d %d %d\n", buffer[0], mouse_x, mouse_y, button, last_mouse_buttons);*/

	last_mouse_buttons = button;
	if (!current_virtual_device) return;
	if (!fb_active) button = B_MOVE;
	if (current_virtual_device->mouse_handler) current_virtual_device->mouse_handler(current_virtual_device, mouse_x, mouse_y, button);
	redraw_mouse();
	EINTRLOOP(rs, fsync(fb_handle));
}

static int handle_fb_mouse(void)
{
	Gpm_Connect conn;

	fb_hmice = c_open(cast_uchar "/dev/input/mice", O_RDWR | O_NOCTTY);
	if (fb_hmice != -1) {
		unsigned char c;
		static const unsigned char imps_seq[] = { 0xf3, 200, 0xf3, 100, 0xf3, 80, 0xf2 };
		static const unsigned char imex_seq[] = { 0xf3, 200, 0xf3, 200, 0xf3, 80, 0xf2 };
		if (hard_write(fb_hmice, imps_seq, sizeof(imps_seq)) != sizeof(imps_seq))
			goto gpm_fallback;
		do
			if (hard_read(fb_hmice, &c, 1) != 1)
				goto gpm_fallback;
		while (c != 0xfa);
		if (hard_read(fb_hmice, &ps2_mouse_mode, 1) != 1)
			goto gpm_fallback;
		if (!ps2_mouse_mode)
			goto open_gpm;
		if (ps2_mouse_mode != 3)
			goto gpm_fallback;

		if (hard_write(fb_hmice, imex_seq, sizeof(imex_seq)) != sizeof(imex_seq))
			goto gpm_fallback;
		do
			if (hard_read(fb_hmice, &c, 1) != 1)
				goto gpm_fallback;
		while (c != 0xfa);
		if (hard_read(fb_hmice, &ps2_mouse_mode, 1) != 1)
			goto gpm_fallback;
		if (ps2_mouse_mode == 3)
			goto open_gpm;
		if (ps2_mouse_mode != 4)
			goto gpm_fallback;
	}

open_gpm:
	if (0) {
gpm_fallback:
		close_socket(&fb_hmice);
	}

	fb_hgpm = -2;
	conn.eventMask = (unsigned short)~0U;
	conn.defaultMask = gpm_smooth;
	conn.minMod = 0;
	conn.maxMod = (unsigned short)~0U;
	save_gpm_signals();
	fb_hgpm = Gpm_Open(&conn, 0);
	restore_gpm_signals();
	if (fb_hgpm < 0) {
		fb_hgpm = -2;
		if (fb_hmice >= 0)
			goto done;
		unhandle_fb_mouse();
		return -1;
	}
done:
	if (fb_hgpm >= 0)
		set_handlers(fb_hgpm, fb_gpm_in, (void (*)(void *))NULL, NULL);
	if (fb_hmice != -1)
		set_handlers(fb_hmice, fb_ps2_in, (void (*)(void *))NULL, NULL);
#ifdef SIGTSTP
	install_signal_handler(SIGTSTP, sig_tstp, NULL, 0);
#endif
#ifdef SIGCONT
	install_signal_handler(SIGCONT, sig_cont, NULL, 0);
#endif
#ifdef SIGTTIN
	install_signal_handler(SIGTTIN, sig_tstp, NULL, 0);
#endif

	return 0;
}

static void unhandle_fb_mouse(void)
{
	if (fb_hgpm >= 0) set_handlers(fb_hgpm, (void (*)(void *))NULL, (void (*)(void *))NULL, NULL);
	if (fb_hgpm >= -1) {
		save_gpm_signals();
		Gpm_Close();
		restore_gpm_signals();
	}
	fb_hgpm = -2;

	close_socket(&fb_hmice);
#ifdef SIGTSTP
	install_signal_handler(SIGTSTP, sig_tstp, NULL, 0);
#endif
#ifdef SIGCONT
	install_signal_handler(SIGCONT, sig_cont, NULL, 0);
#endif
#ifdef SIGTTIN
	install_signal_handler(SIGTTIN, sig_tstp, NULL, 0);
#endif
}

static const unsigned char seq_hide_cursor[] = "\033[10000B\033[10000C\033[?25l";
static const unsigned char seq_show_cursor[] = "\033[10000D\033[?25h";

static void fb_print(const unsigned char *str)
{
	int wr;
	EINTRLOOP(wr, (int)write(TTY, str, strlen(cast_const_char str)));
	if (wr <= 0)
		EINTRLOOP(wr, (int)write(1, str, strlen(cast_const_char str)));
}

static void fb_hide_cursor(void)
{
	fb_print(seq_hide_cursor);
}

static void fb_show_cursor(void)
{
	fb_print(seq_show_cursor);
}

static void fb_pan_display(void)
{
	int rs;
	vi.xoffset=0;
	vi.yoffset=0;
	EINTRLOOP(rs, ioctl(fb_handle, FBIOPAN_DISPLAY, &vi));
	/* don't check error */
}

static unsigned char *fb_init_driver(unsigned char *param, unsigned char *ignore)
{
	char *se;
	unsigned char *e;
	struct stat st;
	int rs;
	unsigned long ul;

#if 0
	if (getenv("LINKS_MEMCPY_TEST")) {
#define LEN		256
		unsigned long iteration = 0;
		size_t ps = getpagesize();
		unsigned char val = 0;
		unsigned char data[LEN];
		unsigned char test[LEN];
		unsigned char *map;
		unsigned char *from;
		unsigned start, end, i;
		memset(data, 0, LEN);
		memset(test, 0, LEN);
		map = mmap(NULL, ps * 3, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
		if (map == MAP_FAILED) perror("mmap"), exit(1);
		map += ps;
		if (mprotect(map, ps, PROT_READ | PROT_WRITE)) perror("mprotect"), exit(1);
		fprintf(stderr, "testing memcpy...\n");
		while (1) {
			start = (unsigned)random() % LEN;
			end = (unsigned)random() % LEN;
			if (start > end)
				continue;
			/*if ((end - start) < 7) continue;*/
			if (random() & 1)
				from = map + (unsigned)random() % 8;
			else
				from = map + ps - (end - start) - (unsigned)random() % 8;
			if (1) {
				for (i = start; i < end; i++)
					data[i] = val++;
				memcpy(from, data + start, end - start);
				/*fprintf(stderr, "copy %lu: %p, %p, %x\n", iteration, test + start, from, end - start);*/
				memcpy_to_fb(test + start, from, end - start, 1);
				if (memcmp(test, data, LEN))
					break;
			}
			iteration++;
		}
		internal_error("memcpy is broken (%lu): %u, %u (%u), %p", iteration, start, end, end - start, from);
	}
#undef LEN
#endif

	save_terminal();

	TTY = 0;
	EINTRLOOP(rs, ioctl(TTY, VT_GETMODE, &vt_omode));
	if (rs == -1) {
		TTY = 1;
		EINTRLOOP(rs, ioctl(TTY, VT_GETMODE, &vt_omode));
		if (rs == -1) {
			TTY = 0;
		}
	}

	kbd_set_raw = 1;
	fb_old_vd = NULL;
	fb_driver_param=NULL;
	if(param != NULL)
		fb_driver_param=stracpy(param);

	border_left = border_right = border_top = border_bottom = 0;
	if (!param) param=cast_uchar "";
	if (*param) {
		if (*param < '0' || *param > '9') {
			bad_p:
			e = stracpy(cast_uchar "-mode syntax is left_border[,top_border[,right_border[,bottom_border]]]\n");
			goto fail1;
		}
		ul = strtoul(cast_const_char param, &se, 10);
		param = cast_uchar se;
		if (ul > MAXINT / 10) goto bad_p;
		border_left = (int)ul;
		if (*param == ',') param++;
	} else {
		border_left = 0;
	}
	if (*param) {
		if (*param < '0' || *param > '9') goto bad_p;
		ul = strtoul(cast_const_char param, &se, 10);
		param = cast_uchar se;
		if (ul > MAXINT / 10) goto bad_p;
		border_top = (int)ul;
		if (*param == ',') param++;
	} else {
		border_top = border_left;
	}
	if (*param) {
		if (*param < '0' || *param > '9') goto bad_p;
		ul = strtoul(cast_const_char param, &se, 10);
		param = cast_uchar se;
		if (ul > MAXINT / 10) goto bad_p;
		border_right = (int)ul;
		if (*param == ',') param++;
	} else {
		border_right = border_left;
	}
	if (*param) {
		if (*param < '0' || *param > '9') goto bad_p;
		ul = strtoul(cast_const_char param, &se, 10);
		param = cast_uchar se;
		if (ul > MAXINT / 10) goto bad_p;
		border_bottom = (int)ul;
		if (*param == ',') param++;
	} else {
		border_bottom = border_top;
	}
	if (*param) goto bad_p;

	EINTRLOOP(rs, fstat(TTY, &st));
	if (rs) {
		e = stracpy(cast_uchar "Cannon stat stdin.\n");
		goto fail1;
	}

	fb_console = (int)(st.st_rdev & 0xff);

	fb_hide_cursor();

	if ((e = fb_switch_init())) {
		goto fail2;
	}

	fb_handle = c_open(cast_uchar "/dev/fb0", O_RDWR | O_NOCTTY);
	if (fb_handle == -1) {
		e = stracpy(cast_uchar "Cannot open /dev/fb0.\n");
		goto fail3;
	}

	EINTRLOOP(rs, ioctl(fb_handle, FBIOGET_VSCREENINFO, &vi));
	if (rs == -1) {
		e = stracpy(cast_uchar "Cannot get FB VSCREENINFO.\n");
		goto fail4;
	}

	/*oldmode=vi;*/

	EINTRLOOP(rs, ioctl(fb_handle, FBIOGET_FSCREENINFO, &fi));
	if (rs == -1) {
		e = stracpy(cast_uchar "Cannot get FB FSCREENINFO.\n");
		goto fail4;
	}

#if 0
	{
	unsigned i, j, l;
	fprintf(stderr, "\n");
	for (i = 0; i < sizeof(fi); i += 16) {
		l = sizeof(fi) - i;
		if (l > 16) l = 16;
		fprintf(stderr, "fi(%02x):", i);
		for (j = 0; j < l; j++) fprintf(stderr, "%s %02x", j && !(j & 3) ? " |" : "", ((unsigned char *)&fi)[i + j]);
		fprintf(stderr, "\n");
	}
	fprintf(stderr, "\n");
	for (i = 0; i < sizeof(vi); i += 16) {
		l = sizeof(vi) - i;
		if (l > 16) l = 16;
		fprintf(stderr, "vi(%02x):",i);
		for (j = 0; j < l; j++) fprintf(stderr, "%s %02x", j && !(j & 3) ? " |" : "", ((unsigned char *)&vi)[i + j]);
		fprintf(stderr, "\n");
	}
	}
#endif

	fb_xsize = vi.xres;
	fb_ysize = vi.yres;
	fb_bits_pp = vi.bits_per_pixel;
	if (fb_bits_pp == 16 && vi.green.length == 5) fb_bits_pp = 15;

	if (fb_xsize <= border_left + border_right) border_left = border_right = 0;
	fb_xsize -= border_left + border_right;
	if (fb_ysize <= border_top + border_bottom) border_top = border_bottom = 0;
	fb_ysize -= border_top + border_bottom;

	fb_driver.x=fb_xsize;
	fb_driver.y=fb_ysize;

	switch (fb_bits_pp) {
#if 0
		case 4:
			fb_pixelsize = 1;
			fb_palette_colors = 16;
			break;
#endif

		case 8:
			fb_pixelsize = 1;
			fb_palette_colors = 256;
			break;

		case 15:
		case 16:
			fb_pixelsize = 2;
			fb_palette_colors = 64;
			break;

		case 24:
			fb_palette_colors = 256;
			fb_pixelsize = 3;
			break;

		case 32:
			fb_palette_colors = 256;
			fb_pixelsize = 4;
			fb_bits_pp = 24;
			break;

		default:
			e = stracpy(cast_uchar "Unknown bit depth.\n");
			goto fail4;
	}
	fb_colors = 1 << fb_bits_pp;

	/* we must pan before setting palette */
	fb_pan_display();

	have_cmap = 0;
	if (fi.visual == FB_VISUAL_PSEUDOCOLOR && fb_colors <= 0x1000000) {/* set palette */
		have_cmap = 1;
		fb_palette_colors = fb_colors;
	}
	if (fi.visual == FB_VISUAL_DIRECTCOLOR) { /* set pseudo palette */
		have_cmap = 2;
	}

	fb_linesize = fi.line_length;
	fb_mem_size = fi.smem_len;

	init_virtual_devices(&fb_driver, NUMBER_OF_DEVICES);
	fb_kbd = handle_svgalib_keyboard(fb_key_in);

	/* Mikulas: nechodi to na sparcu */
	if (fb_mem_size < (unsigned)((border_top + fb_ysize + border_bottom) * fb_linesize)) {
		e = stracpy(cast_uchar "Nonlinear mapping of graphics memory not supported.\n");
		goto fail5;
	}

	if (vi.nonstd) {
		e = stracpy(cast_uchar "Non-standard pixel format.\n");
		goto fail5;
	}

	fb_driver.flags |= GD_DONT_USE_SCROLL;
#ifdef USE_FB_ACCEL
	need_accel_sync = 0;
	if (getenv("LINKS_NO_FB_ACCEL")) {
		rs = -1;
	} else {
		EINTRLOOP(rs, ioctl(fb_handle, FBIO_ACCEL_SUPPORT));
	}
	if (rs < 0) {
		accel_flags = 0;
		/*debug("accel not supported");*/
	} else {
		accel_flags = rs;
		/*debug("accel supported, flags %x", accel_flags);*/
	}
	if (fb_bits_pp != 8)
		accel_flags &= ~(FB_ACCEL_FILLRECT_SUPPORTED | FB_ACCEL_FILLRECT_ACCELERATED);
	if (accel_flags & FB_ACCEL_COPYAREA_ACCELERATED)
		fb_driver.flags &= ~GD_DONT_USE_SCROLL;
#endif

	/*
	 * Some framebuffer implementations (for example Mach64) on Sparc64 hate
	 * partial framebuffer mappings.
	 *
	 * For others, we can save virtual memory space by doing a partial mmap.
	 */
	fb_mapped_size = (border_top + fb_ysize + border_bottom) * fb_linesize;
retry1:
	if ((fb_mem = mmap(NULL, fb_mapped_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_handle, 0)) == MAP_FAILED) {
		if (errno == EINTR) goto retry1;
		if (fb_mapped_size != fb_mem_size) {
			fb_mapped_size = fb_mem_size;
			goto retry1;
		}
		e = stracpy(cast_uchar "Cannot mmap graphics memory.\n");
		goto fail5;
	}
	fb_vmem = fb_mem + border_left * fb_pixelsize + border_top * fb_linesize;
	fb_driver.depth = fb_pixelsize & 7;
	fb_driver.depth |= (fb_bits_pp & 31) << 3;
#if 0
	fprintf(stderr, "\nfb: bytes %u, bits %u\n", fb_pixelsize, fb_bits_pp);
	fprintf(stderr, "r: %x %x %x\n", vi.red.offset, vi.red.length, vi.red.msb_right);
	fprintf(stderr, "g: %x %x %x\n", vi.green.offset, vi.green.length, vi.green.msb_right);
	fprintf(stderr, "b: %x %x %x\n", vi.blue.offset, vi.blue.length, vi.blue.msb_right);
	fprintf(stderr, "t: %x %x %x\n", vi.transp.offset, vi.transp.length, vi.transp.msb_right);
#endif
	if (big_endian) {
		/* Big endian */
		if (fb_driver.depth == 130 || fb_driver.depth == 122) fb_driver.depth |= 1 << 8;
		else if (fb_driver.depth == 196) fb_driver.depth |= 1 << 9;
	}
	if (fb_driver.depth == 65 && have_cmap) {
		if (!fb_driver.param->palette_mode)
			fb_driver.depth |= 0x300;
		fb_driver.flags |= GD_SELECT_PALETTE;
	} else {
		fb_driver.flags &= ~GD_SELECT_PALETTE;
	}

	fb_driver.get_color = get_color_fn(fb_driver.depth);
	if (!fb_driver.get_color) {
		e = stracpy(cast_uchar "Unknown bit format.\n");
		goto fail6;
	}

	if (have_cmap) {
		get_palette(&old_palette);
		generate_palette(&global_pal);
		set_palette(&global_pal);
	}

	install_signal_handler(SIGINT, fb_ctrl_c, fb_kbd, 0);

	/* mouse */
	mouse_buffer = mem_alloc(fb_pixelsize * arrow_area);
	background_buffer = mem_alloc(fb_pixelsize * arrow_area);
	new_background_buffer = mem_alloc(fb_pixelsize * arrow_area);
	background_x = mouse_x = fb_xsize >> 1;
	background_y = mouse_y = fb_ysize >> 1;
	mouse_black = fb_driver.get_color(0);
	mouse_white = fb_driver.get_color(0xffffff);
	mouse_graphics_device = fb_driver.init_device();
	virtual_devices[0] = NULL;
	global_mouse_hidden = 1;
	last_mouse_buttons = B_MOVE;
	handle_fb_mouse();
	/* hide cursor */
	if (border_left | border_top | border_right | border_bottom) fb_clear_videoram();

	show_mouse();

	END_GR

#if 0
	if (getenv("LINKS_MEMCPY_BENCH")) {
		unsigned array2[20000];
		unsigned i;
		for (i = 0; i < 327680; i++) {
			/*memcpy_to_fb((unsigned char *)fb_mem, (unsigned char *)array2 + 0, 2048);*/
			/*memcpy_to_fb((unsigned char *)fb_mem + (i & 31), (unsigned char *)array2 + ((i >> 5) & 31), 11000);*/
			/*memcpy_to_fb((unsigned char *)fb_mem + (i & 31), (unsigned char *)array2 + ((i >> 5) & 31), ((i >> 10) & 31) + 1, 1);*/
			memcpy_to_fb((unsigned char *)array2 + ((i >> 5) & 31), (unsigned char *)fb_mem + (i & 31), ((i >> 10) & 8191) + 1, 1);
		}
		exit(1);
	}
#endif

	return NULL;

#if 0
fail7:
	fb_driver.shutdown_device(mouse_graphics_device);
	mem_free(mouse_buffer);
	mem_free(background_buffer);
	mem_free(new_background_buffer);
	fb_shutdown_palette();
#endif
fail6:
	EINTRLOOP(rs, munmap(fb_mem, fb_mapped_size));
fail5:
	svgalib_free_trm(fb_kbd);
	shutdown_virtual_devices();
fail4:
	EINTRLOOP(rs, fsync(fb_handle));
	close_socket(&fb_handle);
	restore_terminal();
fail3:
	fb_switch_shutdown();
fail2:
	fb_show_cursor();
fail1:
	if (fb_driver_param) { mem_free(fb_driver_param); fb_driver_param=NULL; }
	return e;
}

static void fb_shutdown_driver(void)
{
	int rs;
	mem_free(mouse_buffer);
	mem_free(background_buffer);
	mem_free(new_background_buffer);
	fb_driver.shutdown_device(mouse_graphics_device);
	unhandle_fb_mouse();
	INC_IN_GR
	if (fb_active) {
		fb_clear_videoram();
		/*EINTRLOOP(rs, ioctl(fb_handle, FBIOPUT_VSCREENINFO, &oldmode));*/
	}
	fb_shutdown_palette();
	EINTRLOOP(rs, fsync(fb_handle));
	close_socket(&fb_handle);
	EINTRLOOP(rs, munmap(fb_mem, fb_mapped_size));
	shutdown_virtual_devices();
	fb_switch_shutdown();
	svgalib_free_trm(fb_kbd);
	if(fb_driver_param) mem_free(fb_driver_param);
	/* show cursor */
	fb_show_cursor();
	install_signal_handler(SIGINT, (void (*)(void *))NULL, NULL, 0);

	if (in_gr_operation) internal_error("fb_shutdown_driver: in_gr_operation %d", in_gr_operation);

	restore_terminal();
}

static void fb_after_fork(void)
{
	int rs;
	if (fb_handle != -1) {
		EINTRLOOP(rs, close(fb_handle));
		fb_handle = -1;
	}
}

static unsigned char *fb_get_driver_param(void)
{
	return fb_driver_param;
}

static void fb_get_margin(int *left, int *right, int *top, int *bottom)
{
	*left = border_left;
	*right = border_right;
	*top = border_top;
	*bottom = border_bottom;
}

static int fb_set_margin(int left, int right, int top, int bottom)
{
	struct rect_set *rs;
	int i, l;
	struct rect r;

	if (left + right >= fb_xsize + border_left + border_right)
		return -1;
	if (top + bottom >= fb_ysize + border_top + border_bottom)
		return -1;

	hide_mouse();

	rs = init_rect_set();
	add_to_rect_set(&rs, &mouse_graphics_device->size);
	r.x1 = left - border_left;
	r.y1 = top - border_top;
	r.x2 = fb_xsize - (right - border_right);
	r.y2 = fb_ysize - (bottom - border_bottom);
	exclude_rect_from_set(&rs, &r);
	do_with_mouse_device(
	for (i = 0; i < rs->m; i++) if (is_rect_valid(&rs->r[i]))
		drv->fill_area(current_virtual_device, rs->r[i].x1, rs->r[i].y1, rs->r[i].x2, rs->r[i].y2, 0);
	);
	mem_free(rs);

	mouse_x += border_left - left;
	mouse_y += border_top - top;
	fb_xsize = fb_xsize + border_left + border_right - (left + right);
	fb_ysize = fb_ysize + border_top + border_bottom - (top + bottom);
	border_left = left;
	border_right = right;
	border_top = top;
	border_bottom = bottom;

	if (mouse_x >= fb_xsize) mouse_x = fb_xsize - 1;
	if (mouse_x < 0) mouse_x = 0;
	if (mouse_y >= fb_ysize) mouse_y = fb_ysize - 1;
	if (mouse_y < 0) mouse_y = 0;

	fb_vmem = fb_mem + border_left * fb_pixelsize + border_top * fb_linesize;

	mouse_graphics_device->size.x2 = mouse_graphics_device->clip.x2 = fb_xsize;
	mouse_graphics_device->size.y2 = mouse_graphics_device->clip.y2 = fb_ysize;

	show_mouse();

	if (fb_driver_param) mem_free(fb_driver_param);
	fb_driver_param = init_str();
	l = 0;
	add_num_to_str(&fb_driver_param, &l, left);
	add_chr_to_str(&fb_driver_param, &l, ',');
	add_num_to_str(&fb_driver_param, &l, top);
	add_chr_to_str(&fb_driver_param, &l, ',');
	add_num_to_str(&fb_driver_param, &l, right);
	add_chr_to_str(&fb_driver_param, &l, ',');
	add_num_to_str(&fb_driver_param, &l, bottom);

	resize_virtual_devices(fb_xsize, fb_ysize);

	return 0;
}

/* Return value:        0 alloced on heap
 *                      1 alloced in vidram
 *                      2 alloced in X server shm
 */
static int fb_get_empty_bitmap(struct bitmap *dest)
{
	dest->data = NULL;
	if (dest->x && (size_t)dest->x * (size_t)dest->y / (size_t)dest->x != (size_t)dest->y)
		return -1;
	if ((size_t)dest->x * (size_t)dest->y > MAX_SIZE_T / 2 / fb_pixelsize)
		return -1;
	dest->data = mem_alloc_mayfail((size_t)dest->x * (size_t)dest->y * fb_pixelsize);
	if (!dest->data)
		return -1;
	dest->skip = (ssize_t)dest->x * fb_pixelsize;
	dest->flags = 0;
	return 0;
}

static void fb_register_bitmap(struct bitmap *bmp)
{
}

static void fb_unregister_bitmap(struct bitmap *bmp)
{
	if (bmp->data)
		mem_free(bmp->data);
}

static void *fb_prepare_strip(struct bitmap *bmp, int top, int lines)
{
	if (!bmp->data)
		return NULL;
	return ((unsigned char *)bmp->data) + bmp->skip * top;
}


static void fb_commit_strip(struct bitmap *bmp, int top, int lines)
{
}


static void fb_draw_bitmap(struct graphics_device *dev, struct bitmap *bmp, int x, int y)
{
	unsigned char *scr_start;
	int rs;
	size_t copy_len;

	CLIP_PREFACE

	accel_sync();

	scr_start = fb_vmem + y * fb_linesize + x * fb_pixelsize;
	copy_len = xs * fb_pixelsize;
	for (; ys; ys--) {
		memcpy_to_fb_inline(scr_start, data, copy_len, 0);
		data += bmp->skip;
		scr_start += fb_linesize;
	}
	END_MOUSE
	END_GR
}


#ifdef USE_FB_ACCEL_FILLRECT
static inline unsigned fb_accel_color(void *ptr)
{
	return *(unsigned char *)ptr;
}
#endif


static void fb_fill_area(struct graphics_device *dev, int x1, int y1, int x2, int y2, long color)
{
	unsigned char *dest;
	int y;
	int rs;

	FILL_CLIP_PREFACE

#ifdef USE_FB_ACCEL_FILLRECT
	if (accel_flags & FB_ACCEL_FILLRECT_ACCELERATED) {
		struct fb_fillrect f;
		f.dx = x1 + border_left;
		f.dy = y1 + border_top;
		f.width = x2 - x1;
		f.height = y2 - y1;
		f.color = fb_accel_color(&color);
		f.rop = ROP_COPY;
		EINTRLOOP(rs, ioctl(fb_handle, FBIO_ACCEL_FILLRECT, &f));
		if (rs < 0) {
			error("fill_area accel failed\n");
			goto no_accel;
		}
		if (accel_flags & FB_ACCEL_SYNC_NEEDED)
			need_accel_sync = 1;
	} else
		no_accel:
#endif
	{
		accel_sync();
		dest=fb_vmem+y1*fb_linesize+x1*fb_pixelsize;
		for (y=y2-y1;y;y--){
			pixel_set(dest,(x2-x1)*fb_pixelsize,&color);
			dest+=fb_linesize;
		}
	}
	END_MOUSE
	END_GR
}


static void fb_draw_hline(struct graphics_device *dev, int x1, int y, int x2, long color)
{
	unsigned char *dest;
	int rs;

	HLINE_CLIP_PREFACE

#ifdef USE_FB_ACCEL_FILLRECT
	if (accel_flags & FB_ACCEL_FILLRECT_ACCELERATED) {
		struct fb_fillrect f;
		f.dx = x1 + border_left;
		f.dy = y + border_top;
		f.width = x2 - x1;
		f.height = 1;
		f.color = fb_accel_color(&color);
		f.rop = ROP_COPY;
		EINTRLOOP(rs, ioctl(fb_handle, FBIO_ACCEL_FILLRECT, &f));
		if (rs < 0) {
			error("draw_hline accel failed\n");
			goto no_accel;
		}
		if (accel_flags & FB_ACCEL_SYNC_NEEDED)
			need_accel_sync = 1;
	} else
		no_accel:
#endif
	{
		accel_sync();
		dest=fb_vmem+y*fb_linesize+x1*fb_pixelsize;
		pixel_set(dest,(x2-x1)*fb_pixelsize,&color);
	}
	END_MOUSE
	END_GR
}


static void fb_draw_vline(struct graphics_device *dev, int x, int y1, int y2, long color)
{
	unsigned char *dest;
	int y;
	int rs;

	VLINE_CLIP_PREFACE

#ifdef USE_FB_ACCEL_FILLRECT
	if (accel_flags & FB_ACCEL_FILLRECT_ACCELERATED) {
		struct fb_fillrect f;
		f.dx = x + border_left;
		f.dy = y1 + border_top;
		f.width = 1;
		f.height = y2 - y1;
		f.color = fb_accel_color(&color);
		f.rop = ROP_COPY;
		EINTRLOOP(rs, ioctl(fb_handle, FBIO_ACCEL_FILLRECT, &f));
		if (rs < 0) {
			error("draw_vline accel failed\n");
			goto no_accel;
		}
		if (accel_flags & FB_ACCEL_SYNC_NEEDED)
			need_accel_sync = 1;
	} else
		no_accel:
#endif
	{
		accel_sync();
		dest = fb_vmem + y1 * fb_linesize + x * fb_pixelsize;
		y = y2 - y1;
		switch (fb_pixelsize) {
			case 1: {
				unsigned char val = *(unsigned char *)&color;
				do {
					*dest = val;
					dest += fb_linesize;
				} while (--y);
				break;
			}
#ifdef t2c
			case 2: {
				t2c val = *(t2c *)memory_barrier(&color);
				do {
					*(t2c *)dest = val;
					dest += fb_linesize;
				} while (--y);
				break;
			}
#endif
#ifdef t4c
			case 4: {
				t4c val = *(t4c *)memory_barrier(&color);
				do {
					*(t4c *)dest = val;
					dest += fb_linesize;
				} while (--y);
				break;
			}
#endif
			default: {
				do {
					memcpy_to_fb(dest, (unsigned char *)&color, fb_pixelsize, 0);
					dest += fb_linesize;
				} while (--y);
				break;
			}
		}
	}
	END_MOUSE
	END_GR
}


static int fb_scroll(struct graphics_device *dev, struct rect_set **ignore, int scx, int scy)
{
	int rs;

	SCROLL_CLIP_PREFACE

#ifdef USE_FB_ACCEL
	if (accel_flags & FB_ACCEL_COPYAREA_ACCELERATED) {
		struct fb_copyarea f;
		if (scx >= 0) {
			f.sx = dev->clip.x1 + border_left;
			f.width = dev->clip.x2 - dev->clip.x1 - scx;
			f.dx = dev->clip.x1 + scx + border_left;
		} else {
			f.sx = dev->clip.x1 - scx + border_left;
			f.width = dev->clip.x2 - dev->clip.x1 + scx;
			f.dx = dev->clip.x1 + border_left;
		}
		if (scy >= 0) {
			f.sy = dev->clip.y1 + border_top;
			f.height = dev->clip.y2 - dev->clip.y1 - scy;
			f.dy = dev->clip.y1 + scy + border_top;
		} else {
			f.sy = dev->clip.y1 - scy + border_top;
			f.height = dev->clip.y2 - dev->clip.y1 + scy;
			f.dy = dev->clip.y1 + border_top;
		}
		EINTRLOOP(rs, ioctl(fb_handle, FBIO_ACCEL_COPYAREA, &f));
		if (rs < 0) {
			error("hscroll accel failed\n");
			goto no_accel;
		}
		if (accel_flags & FB_ACCEL_SYNC_NEEDED)
			need_accel_sync = 1;
	} else
		no_accel:
#endif
	{
		unsigned char *dest, *src;
		int y, len, dest_off, src_off;

		accel_sync();

		if (scx >= 0) {
			len = (dev->clip.x2 - dev->clip.x1 - scx) * fb_pixelsize;
			dest_off = scx * fb_pixelsize;
			src_off = 0;
		} else {
			len = (dev->clip.x2 - dev->clip.x1 + scx) * fb_pixelsize;
			dest_off = 0;
			src_off = -scx * fb_pixelsize;
		}
		if (scy > 0) {
			/* Down */
			dest = fb_vmem + (dev->clip.y2 - 1) * fb_linesize + dev->clip.x1 * fb_pixelsize;
			src = dest - fb_linesize * scy;
			dest += dest_off;
			src += src_off;
			for (y = dev->clip.y2 - dev->clip.y1 - scy; y; y--) {
				memcpy_to_fb(dest, src, len, 1);
				dest -= fb_linesize;
				src -= fb_linesize;
			}
		} else {
			/* Up */
			dest = fb_vmem + dev->clip.y1 * fb_linesize + dev->clip.x1 * fb_pixelsize;
			src = dest - fb_linesize * scy;
			dest += dest_off;
			src += src_off;
			for (y = dev->clip.y2 - dev->clip.y1 + scy; y; y--) {
				if (scy)
					memcpy_to_fb(dest, src, len, 1);
				else
					memmove_in_fb(dest, src, len);
				dest += fb_linesize;
				src += fb_linesize;
			}
		}
	}
	END_MOUSE
	END_GR
	return 1;
}

static void fb_update_palette(void)
{
	int rs;

	if (!fb_driver.param->palette_mode)
		fb_driver.depth |= 0x300;
	else
		fb_driver.depth &= ~0x300;
	fb_driver.get_color = get_color_fn(fb_driver.depth);
	if (!fb_driver.get_color)
		internal_error("invalid driver depth %d", fb_driver.depth);

	mouse_black = fb_driver.get_color(0);
	mouse_white = fb_driver.get_color(0xffffff);

	free_palette(&global_pal);
	generate_palette(&global_pal);

	START_GR
	set_palette(&global_pal);
	END_GR
}

static int fb_block(struct graphics_device *dev)
{
	int rs;
	if (fb_old_vd) return 1;
	INC_IN_GR
	if (fb_active) accel_sync();
	fb_block_dev = dev;
	unhandle_fb_mouse();
	fb_old_vd = current_virtual_device;
	current_virtual_device = NULL;
	svgalib_block_itrm(fb_kbd);
	if (have_cmap && fb_active) set_palette(&old_palette);
	EINTRLOOP(rs, fsync(fb_handle));
	fb_switch_shutdown();
	fb_clear_videoram();
	fb_show_cursor();
	fb_print(cast_uchar "\n");
	restore_terminal();
	return 0;
}

static int fb_unblock(struct graphics_device *dev)
{
	int rs;
	unsigned char *e;
	if (current_virtual_device) return 0;
	if (dev != fb_block_dev) return -2;
	if (svgalib_unblock_itrm(fb_kbd)) return -1;
	e = fb_switch_init();
	if (e) {
		fatal_exit("fb_switch_init failed: %s", e);
	}
	save_terminal();
	fb_hide_cursor();
	fb_pan_display();
	current_virtual_device = fb_old_vd;
	fb_old_vd = NULL;
	if (have_cmap) set_palette(&global_pal);
	handle_fb_mouse();
	END_GR
	if (border_left | border_top | border_right | border_bottom) fb_clear_videoram();
	if (current_virtual_device) current_virtual_device->redraw_handler(current_virtual_device, &current_virtual_device->size);
	EINTRLOOP(rs, fsync(fb_handle));
	return 0;
}


struct graphics_driver fb_driver = {
	cast_uchar "fb",
	fb_init_driver,
	init_virtual_device,
	shutdown_virtual_device,
	fb_shutdown_driver,
	fb_show_cursor,
	fb_after_fork,
	fb_get_driver_param,
	(unsigned char *(*)(void))NULL,
	fb_get_margin,
	fb_set_margin,
	fb_get_empty_bitmap,
	fb_register_bitmap,
	fb_prepare_strip,
	fb_commit_strip,
	fb_unregister_bitmap,
	fb_draw_bitmap,
	(long (*)(int))NULL,	/* fb_get_color */
	fb_fill_area,
	fb_draw_hline,
	fb_draw_vline,
	fb_scroll,
	(void (*)(struct graphics_device *dev))NULL,			/* set_clip_area */
	(void (*)(struct graphics_device *dev))NULL,			/* flush */
	fb_block,
	fb_unblock,
	fb_update_palette,						/* set_palette */
	NULL,								/* get_real_colors */
	(void (*)(struct graphics_device *, unsigned char *))NULL,	/* set_title */
	(int (*)(unsigned char *, int))NULL,				/* exec */
	(void (*)(unsigned char *))NULL,				/* set_clipboard_text */
	(unsigned char *(*)(void))NULL,					/* get_clipboard_text */
	0,				/* depth (filled in fb_init_driver function) */
	0, 0,				/* size (in X is empty) */
	GD_NEED_CODEPAGE,		/* flags */
	NULL,				/* param */
};

#endif /* GRDRV_FB */
