/*
 * Frame buffer driver for Intergraphics CyberPro 50xx.
 */

#include <linux/config.h>
#include <linux/module.h>
#include <linux/fb.h>
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/delay.h>
#include <linux/fb.h>

#include <asm/uaccess.h>

#include "cyber5k.h"

#define DEBUG

#include "cyber5k_tv.c"
#if 0
// FIXME: calculating the memory clock with decode_pll doesn't work
static int mclk = 120;
module_param(mclk, int, 0);
MODULE_PARM_DESC(mclk, "Memory frequency in MHz.");
#endif

static char *mode = NULL;
module_param(mode, charp, 0);
MODULE_PARM_DESC(mode, "Preferred vga video mode (default 640x480-8@60)");

static char *tvnorm = NULL;
module_param(tvnorm, charp, 0);
MODULE_PARM_DESC(tvnorm, "Select 'pal' or 'ntsc' TV output (default 'pal')");

static char *tvmode = NULL;
module_param(tvmode, charp, 0);
MODULE_PARM_DESC(tvmode, "Select tv mode, overrides parameter 'mode'.\n"
		 "\t\tAvailable PAL modes:\n"
		 "\t\t640x400-bpp, 640x440-bpp, 640x480-bpp, 720x480-bpp,\n"
		 "\t\t720x540-bpp, 720x756-bpp, 768x576-bpp, 800x600-bpp,\n"
		 "\t\t856x480-bpp, 976x576-bpp, 1024x576-bpp.\n"
		 "\t\tAvailable NTSC modes:\n"
		 "\t\t640x400-bpp, 640x440-bpp, 640x480-bpp, 720x480-bpp,\n"
		 "\t\t856x480-bpp.\n"
		 "\t\tbpp can be 8, 15, 16, 24 or 32");

static int accel = 1;
module_param(accel, bool, 0);
MODULE_PARM_DESC(accel, "Enable/disable console acceleration (default on).");

/**
 * The dump_mode parameter is useful if you want to define another
 * TV mode. You need to get somebody else to program the mode for you
 * (e.g. the BIOS or XFree86) and then load the framebuffer module
 * with dump_mode=1. Use the information from the syslog to define
 * another entry in the TV mode file cyber5k_tv.c.
 */
static int dump_mode;
module_param(dump_mode, bool, 0);
MODULE_PARM_DESC(dump_mode, "Print the current display mode to console.");

MODULE_DESCRIPTION("CyberPro 50xx framebuffer driver");
MODULE_LICENSE("GPL");

/**
 * Default mode: 640x480, hsync 31.5kHz, vsync 60 Hz
 */
static struct fb_videomode __devinitdata cyber5k_default_mode = {
	.refresh	= 60,
	.xres		= 640,
	.yres		= 480,
	.pixclock	= 39682,
	.left_margin	= 56,
	.right_margin	= 16,
	.upper_margin	= 34,
	.lower_margin	= 9,
	.hsync_len	= 88,
	.vsync_len	= 2,
	.sync		= FB_SYNC_COMP_HIGH_ACT | FB_SYNC_VERT_HIGH_ACT,
	.vmode		= FB_VMODE_NONINTERLACED
};

/**
 * Mode-db of fake modes for predefined tv modes.
 */
static struct fb_videomode cyber5k_fake_videomodes[] = {
	{ NULL, 0, 640, 400, 78125 },
	{ NULL, 0, 640, 440, 71022 },
	{ NULL, 0, 640, 480, 65104 },
	{ NULL, 0, 720, 480, 57870 },
	{ NULL, 0, 720, 540, 51440 },
	{ NULL, 0, 720, 576, 48225 },
	{ NULL, 0, 768, 576, 45211 },
	{ NULL, 0, 800, 600, 41666 },
	{ NULL, 0, 856, 480, 48676 },
	{ NULL, 0, 976, 576, 35576 },
	{ NULL, 0, 1024, 576, 33908 },
};

/**
 * Pointer to tv modes to use (cyber5k_pal_modes or cyber5k_ntsc_modes).
 */
struct cyber5k_tvmode *cyber5k_tv_modes = cyber5k_pal_modes;

/**
 * In, out, and & or byte (8 bit).
 */
static inline void
cyber5k_outb(struct cyber5k_info *c5k, unsigned long offset, unsigned char val)
{
	unsigned long mmio = c5k->mmio_base;
	writeb(val, (void *)(mmio + offset));
}
static inline unsigned char
cyber5k_inb(struct cyber5k_info *c5k, unsigned long offset)
{
	unsigned long mmio = c5k->mmio_base;
	return readb((void *)(mmio + offset));
}
static inline void
cyber5k_andb(struct cyber5k_info *c5k, unsigned long offset, unsigned char and)
{
	unsigned long mmio = c5k->mmio_base;
	writeb(readb((void *)(mmio + offset)) & and, (void *)(mmio + offset));
}
static inline void
cyber5k_orb(struct cyber5k_info *c5k, unsigned long offset, unsigned char or)
{
	unsigned long mmio = c5k->mmio_base;
	writeb(readb((void *)(mmio + offset)) | or, (void *)(mmio + offset));
}

/**
 * In, out, and & or indexed byte (8 bit).
 */
static inline void
cyber5k_outx(struct cyber5k_info *c5k, unsigned long offset,
	     unsigned char ix, unsigned char val)
{
	unsigned long mmio = c5k->mmio_base;
	writeb(ix, (void *)(mmio + offset));
	writeb(val, (void *)(mmio + offset + 1));
}
static inline unsigned char
cyber5k_inx(struct cyber5k_info *c5k, unsigned long offset, unsigned char ix)
{
	unsigned long mmio = c5k->mmio_base;
	writeb(ix, (void *)(mmio + offset));
	return readb((void *)(mmio + offset + 1));
}
static inline void
cyber5k_andx(struct cyber5k_info *c5k, unsigned long offset,
	     unsigned char ix, unsigned char and)
{
	unsigned long mmio = c5k->mmio_base;
	writeb(ix, (void *)(mmio + offset));
	writeb(readb((void *)(mmio + offset + 1)) & and,
	       (void *)(mmio + offset + 1));
}
static inline void
cyber5k_orx(struct cyber5k_info *c5k, unsigned long offset,
	    unsigned char ix, unsigned char or)
{
	unsigned long mmio = c5k->mmio_base;
	writeb(ix, (void *)(mmio + offset));
	writeb(readb((void *)(mmio + offset + 1)) | or,
	       (void *)(mmio + offset + 1));
}

/**
 * In, out word (16 bit).
 */
static inline void
cyber5k_outw(struct cyber5k_info *c5k, unsigned long offset,
	     unsigned short val)
{
	unsigned long mmio = c5k->mmio_base;
	writew(val, (void *)(mmio + offset));
}
static inline unsigned short
cyber5k_inw(struct cyber5k_info *c5k, unsigned long offset)
{
	unsigned long mmio = c5k->mmio_base;
	return readw((void *)(mmio + offset));
}

/**
 * Out doubleword (32 bit).
 */
static inline void
cyber5k_outl(struct cyber5k_info *c5k, unsigned long offset, unsigned int val)
{
	unsigned long mmio = c5k->mmio_base;
	writel(val, (void *)(mmio + offset));
}

#define MKW(low,high) ((high) << 8 | (low))

/**
 * Hardware acceleration & hardware cursor
 */
static int
cyber5k_sync(struct fb_info *info)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;
	int count = 100000;

	if (!(info->var.accel_flags & FB_ACCELF_TEXT))
		return 0;

	while (count-- && (cyber5k_inb(c5k, 0xbf011) & 0x80))
		udelay(1);
	if (count < 0)
		cyber5k_outb(c5k, 0xbf011, 0);
	return 0;
}

static void
cyber5k_copyarea(struct fb_info *info, const struct fb_copyarea *region)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;
	unsigned short cmd = 0x8000;
	unsigned long src, dst;

	if (!(info->var.accel_flags & FB_ACCELF_TEXT)) {
		cfb_copyarea(info, region);
		return;
	}

	cyber5k_outb(c5k, 0xbf011, 0);
	cyber5k_outw(c5k, 0xbf060, region->width - 1);
	cyber5k_outw(c5k, 0xbf062, region->height - 1);

	src = region->sx + region->sy * info->var.xres_virtual;
	dst = region->dx + region->dy * info->var.xres_virtual;

	if (region->sx < region->dx) {
		src += region->width - 1;
		dst += region->width - 1;
		cmd |= 0x0004;
	}

	if (region->sy < region->dy) {
		src += (region->height - 1) * info->var.xres_virtual;
		dst += (region->height - 1) * info->var.xres_virtual;
		cmd |= 0x0002;
	}

	if (info->var.bits_per_pixel == 24) {
		cyber5k_outb(c5k, 0xbf078, dst);
		src *= 3;
		dst *= 3;
	}
	cyber5k_outl(c5k, 0xbf170, src);
	cyber5k_outl(c5k, 0xbf178, dst);
	cyber5k_outb(c5k, 0xbf048, 0x03);

	cyber5k_outw(c5k, 0xbf07c, cmd);
	cyber5k_outw(c5k, 0xbf07e, 0x2000|0x0800);
}

static void
cyber5k_fillrect(struct fb_info *info, const struct fb_fillrect *rect)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;
	unsigned long dst;
	unsigned int col;

	if (!(info->var.accel_flags & FB_ACCELF_TEXT)) {
		cfb_fillrect(info, rect);
		return;
	}

	cyber5k_outb(c5k, 0xbf011, 0);
	cyber5k_outw(c5k, 0xbf060, rect->width - 1);
	cyber5k_outw(c5k, 0xbf062, rect->height - 1);

	col = rect->color;
	if (info->fix.visual == FB_VISUAL_TRUECOLOR ||
	    info->fix.visual == FB_VISUAL_DIRECTCOLOR)
		col = ((unsigned int *) info->pseudo_palette)[col];
	cyber5k_outl(c5k, 0xbf058, col);

	dst = rect->dx + rect->dy * info->var.xres_virtual;
	if (info->var.bits_per_pixel == 24) {
		cyber5k_outb(c5k, 0xbf078, dst);
		dst *= 3;
	}

	cyber5k_outl(c5k, 0xbf178, dst);
	cyber5k_outb(c5k, 0xbf048, 0x03);
	cyber5k_outw(c5k, 0xbf07c, 0x8000);
	cyber5k_outw(c5k, 0xbf07e, 0x0800);
}

/**
 * Hardware cursor acceleration functions.
 */
static inline void
cyber5k_cursor_hide(struct cyber5k_info *c5k)
{
	c5k->cursor_on = 0;
	cyber5k_andx(c5k, 0x3ce, 0x56, 0xfe);
}

static inline void
cyber5k_cursor_show(struct cyber5k_info *c5k)
{
	c5k->cursor_on = 1;
	if (c5k->blanked)
		return;
	cyber5k_orx(c5k, 0x3ce, 0x56, 0x01);
}

static inline void
cyber5k_cursor_setpos(struct cyber5k_info *c5k, int x, int y)
{
	cyber5k_outx(c5k, 0x3ce, 0x50, x);
	cyber5k_outx(c5k, 0x3ce, 0x51, x >> 8);
	cyber5k_outx(c5k, 0x3ce, 0x53, y);
	cyber5k_outx(c5k, 0x3ce, 0x54, y >> 8);
}

static inline void
cyber5k_cursor_setcol(struct cyber5k_info *c5k,
		      unsigned int fg_color, unsigned int bg_color)
{
	struct cyber5k_color *fg, *bg;

	fg = c5k->palette + fg_color;
	bg = c5k->palette + bg_color;
	cyber5k_orx(c5k, 0x3ce, 0x56, 0x04);	/* switch to cursor 3c6-3c9 */
	cyber5k_outb(c5k, 0x3c8, 0x00);	/* Set index for bg color */
	cyber5k_outb(c5k, 0x3c9, bg->red);
	cyber5k_outb(c5k, 0x3c9, bg->green);
	cyber5k_outb(c5k, 0x3c9, bg->blue);
	cyber5k_outb(c5k, 0x3c8, 0x01);	/* Set index for fg color */
	cyber5k_outb(c5k, 0x3c9, fg->red);
	cyber5k_outb(c5k, 0x3c9, fg->green);
	cyber5k_outb(c5k, 0x3c9, fg->blue);
	cyber5k_andx(c5k, 0x3ce, 0x56, 0xfb);	/* switch to vga 3c6-3c9 */
}

static inline void
cyber5k_cursor_load(struct cyber5k_info *c5k, const unsigned char *data,
		    const unsigned char *mask, int rop, int width, int height)
{
	/**
	 * Bitmap format for the hardware cursor is two bits for
	 * each pixel:
	 *   00: cursor color 0 (3cf/56[2].3c8=0, 3cf/56[2].3c9 rgb)
	 *   01: cursor color 1 (3cf/56[2].3c8=1, 3cf/56[2].3c9 rgb)
	 *   10: transparent (the underlying pixel is displayed)
	 *   11: complement (1's complement of the underlying pixel)
	 * The size of the hardware cursor is always 64x64 pixels.
	 * One line of the pixel has 16 bytes. The upper left corner
	 * of cursor bitmap area starts at (3cf/52,3cf/55) and ends
	 * at (63,63).
	 *       _____________________ 
	 *      |                ^    |
	 *      |                |    |
	 *      |           v. preset |
	 *      |                |    |
	 *      |              __v____|
	 *      |             |       |
	 *      |<----------->|       |
	 *      |  h. preset  |       |
	 *      |_____________|_______|
	 */
	static const u8 bits_lookup[16] = {
		0x00, 0x40, 0x10, 0x50, 0x04, 0x44, 0x14, 0x54,
		0x01, 0x41, 0x11, 0x51, 0x05, 0x45, 0x15, 0x55
	};

	static const u8 mask_lookup[16] = {
		0xaa, 0x2a, 0x8a, 0x0a, 0xa2, 0x22, 0x82, 0x02,
		0xa8, 0x28, 0x88, 0x08, 0xa0, 0x20, 0x80, 0x00
	};
	unsigned int bpl = (width + 7) >> 3;
	unsigned char *bitmap;
	unsigned int i, j;

	cyber5k_outw(c5k, 0x3ce, MKW(0x52, 64 - (bpl << 3)));	/* h. preset */
	cyber5k_outw(c5k, 0x3ce, MKW(0x55, 64 - height));	/* v. preset */
	bitmap = (unsigned char *) c5k->smem_base + c5k->cursor_offset;
	bitmap += (64 - height)*16 + (8 - bpl)*2;
	for (i = 0; i < height; i++) {
		for (j = 0; j < bpl; j++) {
			unsigned char d = *data++;
			unsigned char m = *mask++;
			switch (rop) {
			case ROP_XOR:
				*bitmap++ = mask_lookup[m >> 4] |
					bits_lookup[(d ^ m) >> 4];
				*bitmap++ = mask_lookup[m & 15] |
					bits_lookup[(d ^ m) & 15];
				break;
			case ROP_COPY:
				*bitmap++ = mask_lookup[m >> 4] | 
					bits_lookup[(d & m) >> 4];
				*bitmap++ = mask_lookup[m & 15] |
					bits_lookup[(d & m) & 15];
				break;
			}
		}
		bitmap += 16 - bpl*2;
	}
}

static void
cyber5k_cursor_init(struct cyber5k_info *c5k)
{
	static unsigned char init_data[16] = {
		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
	};
	static unsigned char init_mask[16] = {
		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
	};

	cyber5k_cursor_hide(c5k);
	cyber5k_orx(c5k, 0x3ce, 0x56, 0x02);		/* 64x64 pixels */
	cyber5k_outw(c5k, 0x3ce, MKW(0xf9,0x07));	/* cursor timing */
	cyber5k_outx(c5k, 0x3ce, 0x7e, c5k->cursor_offset >> 10);
	cyber5k_outx(c5k, 0x3ce, 0x7f, c5k->cursor_offset >> 18);
	cyber5k_cursor_setpos(c5k, 0, 0);
	cyber5k_cursor_load(c5k, init_data, init_mask, ROP_COPY, 8, 16);
}

static int
cyber5k_cursor(struct fb_info *info, struct fb_cursor *cursor)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;
	unsigned short set = cursor->set;
	unsigned long offset;

	cyber5k_cursor_hide(c5k);

	if (cursor->image.width > 64 ||
	    cursor->image.height > 64 ||
	    cursor->image.depth != 1)
		return soft_cursor(info, cursor);

	/* If XFree killed the cursor - restore it */
	offset = (unsigned long) (cyber5k_inx(c5k, 0x3ce, 0x7e)) << 10;
	offset += (unsigned long) (cyber5k_inx(c5k, 0x3ce, 0x7f)) << 18;
	if (offset != c5k->cursor_offset) {
		cyber5k_cursor_init(c5k);
		set = FB_CUR_SETPOS | FB_CUR_SETCMAP | FB_CUR_SETIMAGE;
	}

	if (set & FB_CUR_SETPOS)
		cyber5k_cursor_setpos(c5k,
				      cursor->image.dx - info->var.xoffset,
				      cursor->image.dy - info->var.yoffset);

	if (set & FB_CUR_SETCMAP)
		cyber5k_cursor_setcol(c5k, cursor->image.fg_color,
				      cursor->image.bg_color);

	if (set & (FB_CUR_SETSIZE | FB_CUR_SETSHAPE | FB_CUR_SETIMAGE))
		cyber5k_cursor_load(c5k, cursor->image.data, cursor->mask,
				    cursor->rop, cursor->image.width,
				    cursor->image.height);

	if (cursor->enable)
		cyber5k_cursor_show(c5k);

	return 0;
}

/**
 * Some low level primitives
 */
static void
cyber5k_ramdac_on(struct cyber5k_info *c5k)
{
	cyber5k_orx(c5k, 0x3ce, 0x56, 0x04);	/* switch to cursor 3c6-3c9 */
	cyber5k_andb(c5k, 0x3c6, 0xbf);		/* DAC power on. */
	cyber5k_andx(c5k, 0x3ce, 0x56, 0xfb);	/* switch to vga 3c6-3c9 */
}

static void
cyber5k_ramdac_off(struct cyber5k_info *c5k)
{
	cyber5k_orx(c5k, 0x3ce, 0x56, 0x04);	/* switch to cursor 3c6-3c9 */
	cyber5k_orb(c5k, 0x3c6, 0x40);		/* DAC power off. */
	cyber5k_andx(c5k, 0x3ce, 0x56, 0xfb);	/* switch to vga 3c6-3c9 */
}

static void
cyber5k_vsync_wait(struct cyber5k_info *c5k)
{
	while ( (cyber5k_inb(c5k, 0x3da) & 8));	/* Wait for vsync on */
	while (!(cyber5k_inb(c5k, 0x3da) & 8));	/* Wait for vsync off*/
}

static void
cyber5k_toggle_clock(struct cyber5k_info *c5k)
{
	int i;

	/* Reset VCLK PLL. */
	cyber5k_outw(c5k, 0x3ce, MKW(0xbc, 0x01));
	cyber5k_outw(c5k, 0x3ce, MKW(0xbc, 0x00));

	/* Toggle bit 0x80 in 3cf/b9 */
	cyber5k_andx(c5k, 0x3ce, 0xb9, 0x7f);
	cyber5k_orx(c5k, 0x3ce, 0xb9, 0x80);
	cyber5k_andx(c5k, 0x3ce, 0xb9, 0x7f);

	/* Wait for 5 vertical syncs. */
	for (i = 0; i < 5; i++)
		cyber5k_vsync_wait(c5k);
}

static void
cyber5k_toggle_ram(struct cyber5k_info *c5k)
{
	int i;

	/* Reset memory clock PLL. */
	cyber5k_outw(c5k, 0x3ce, MKW(0xbd, 0x01));
	cyber5k_outw(c5k, 0x3ce, MKW(0xbd, 0x00));

	/* Reset SG engine. */
	cyber5k_andx(c5k, 0x3c4, 0x30, 0x7f);
	cyber5k_orx(c5k, 0x3c4, 0x30, 0x80);

	/* Flush fifo queues. */
	cyber5k_outw(c5k, 0x3ce, MKW(0x70, 0x4b));
	for (i = 0; i < 16; i++)
		cyber5k_vsync_wait(c5k);
	cyber5k_outw(c5k, 0x3ce, MKW(0x70, 0x0b));
}

static void
cyber5k_reset_ram(struct cyber5k_info *c5k)
{
	/* Reset sequencer. */
	cyber5k_outw(c5k, 0x3ce, MKW(0x70, 0xcb));

	/* Set memory style and size. */
#if 0
	// FIXME: howto find out memory organization (memory detection)??
	cyber5k_outw(c5k, 0x3ce, MKW(0x71, 0xc8));  /* 512Kx32 sgram */
	cyber5k_outw(c5k, 0x3ce, MKW(0x72, 0x06));  /* 4MB, 64 bit bus */
	cyber5k_outw(c5k, 0x3ce, MKW(0x74, 0x1b));  /* fifo delta m value */
	cyber5k_outw(c5k, 0x3ce, MKW(0x75, 0x1e));  /* fifo delta g value */
	cyber5k_outw(c5k, 0x3ce, MKW(0x79, 0x30));  /* late to cas */
	cyber5k_outw(c5k, 0x3ce, MKW(0xf8,0x04));   /* edo memory ctrl */
	cyber5k_outw(c5k, 0x3ce, MKW(0xf9,0x07));   /* bus address delay */
#endif
	cyber5k_outw(c5k, 0x3ce, MKW(0x90, 0x03));  /* latdly0/gcextpath */

	/* Set memory clock frequency. */
	cyber5k_outx(c5k, 0x3ce, 0xb2, c5k->mclk[0]);
	cyber5k_outx(c5k, 0x3ce, 0xb3, c5k->mclk[1]);
	cyber5k_outx(c5k, 0x3ce, 0xbb, c5k->mclk[2]);

	/* SG ram control registers */
	cyber5k_outw(c5k, 0x3c4, MKW(0x30,0x85));
	cyber5k_outw(c5k, 0x3c4, MKW(0x31,0x02));
	cyber5k_outw(c5k, 0x3c4, MKW(0x32,0x00));
	cyber5k_outw(c5k, 0x3c4, MKW(0x33,0x00));
	cyber5k_outw(c5k, 0x3c4, MKW(0x34,0x20));
	cyber5k_outw(c5k, 0x3c4, MKW(0x35,0x00));
	cyber5k_outw(c5k, 0x3c4, MKW(0x36,0x3d));
	cyber5k_outw(c5k, 0x3c4, MKW(0x37,0x00));
	cyber5k_outw(c5k, 0x3c4, MKW(0x38,0x80));
	cyber5k_outw(c5k, 0x3c4, MKW(0x39,0x00));
	cyber5k_outw(c5k, 0x3c4, MKW(0x3a,0x08));
	cyber5k_outw(c5k, 0x3c4, MKW(0x3b,0x00));
	cyber5k_outw(c5k, 0x3c4, MKW(0x3c,0xa1));
	cyber5k_outw(c5k, 0x3c4, MKW(0x3d,0x3c));
	cyber5k_outw(c5k, 0x3c4, MKW(0x3e,0x0e));
	cyber5k_outw(c5k, 0x3c4, MKW(0x3f,0x55));

	cyber5k_toggle_ram(c5k);
}

/**
 * Print information about current timing to the console log.
 * Included registers are vclk, display and tv registers.
 */
static void
cyber5k_print_mode(struct cyber5k_info *c5k)
{
	unsigned int i;

	printk(KERN_INFO "vclk registers:\n");
	printk(KERN_INFO "3cf/b0 = 0x%02x\n", cyber5k_inx(c5k, 0x3ce, 0xb0));
	printk(KERN_INFO "3cf/b1 = 0x%02x\n", cyber5k_inx(c5k, 0x3ce, 0xb1));
	printk(KERN_INFO "3cf/ba = 0x%02x\n", cyber5k_inx(c5k, 0x3ce, 0xba));

	printk(KERN_INFO "crtc registers:\n");
	printk(KERN_INFO "3d5/00 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x00));
	printk(KERN_INFO "3d5/01 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x01));
	printk(KERN_INFO "3d5/02 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x02));
	printk(KERN_INFO "3d5/03 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x03));
	printk(KERN_INFO "3d5/04 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x04));
	printk(KERN_INFO "3d5/05 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x05));
	printk(KERN_INFO "3d5/06 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x06));
	printk(KERN_INFO "3d5/07 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x07));
	printk(KERN_INFO "3d5/08 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x08));
	printk(KERN_INFO "3d5/09 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x09));
	printk(KERN_INFO "3d5/10 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x10));
	printk(KERN_INFO "3d5/11 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x11));
	printk(KERN_INFO "3d5/12 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x12));
	printk(KERN_INFO "3d5/13 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x13));
	printk(KERN_INFO "3d5/14 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x14));
	printk(KERN_INFO "3d5/15 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x15));
	printk(KERN_INFO "3d5/16 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x16));
	printk(KERN_INFO "3d5/17 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x17));
	printk(KERN_INFO "3d5/18 = 0x%02x\n", cyber5k_inx(c5k, 0x3d4, 0x18));
	printk(KERN_INFO "3ce/11 = 0x%02x\n", cyber5k_inx(c5k, 0x3ce, 0x11));

	printk(KERN_INFO "tv registers:\n");
	/* 66 16-bit values in range 0xbe428 .. 0xbe52c (stride 4). */
	for (i = 0xbe428; i < 0xbe528; i += 16)
		printk(KERN_INFO "/* 0x%05x: */"
		       " 0x%04x, 0x%04x, 0x%04x, 0x%04x\n", i,
		       cyber5k_inw(c5k, i), cyber5k_inw(c5k, i + 4),
		       cyber5k_inw(c5k, i + 8), cyber5k_inw(c5k, i + 12));
	printk(KERN_INFO "/* 0x%05x: */ 0x%04x, 0x%04x\n", i,
	       cyber5k_inw(c5k, i), cyber5k_inw(c5k, i + 4));
	/* 34 16-bit values in range 0xbe530 .. 0xbe572. */
	for (i = 0xbe530; i < 0xbe570; i += 8)
		printk(KERN_INFO "/* 0x%05x: */"
		       " 0x%04x, 0x%04x, 0x%04x, 0x%04x\n", i,
		       cyber5k_inw(c5k, i), cyber5k_inw(c5k, i + 2),
		       cyber5k_inw(c5k, i + 4), cyber5k_inw(c5k, i + 6));
	printk(KERN_INFO "/* 0x%05x: */ 0x%04x, 0x%04x\n", i,
	       cyber5k_inw(c5k, i), cyber5k_inw(c5k, i + 2));
	/* 84 16-bit values in range 0xbe574 .. 0xbe61a. */
	for (i = 0xbe574; i < 0xbe61c; i += 8)
		printk(KERN_INFO "/* 0x%05x: */"
		       " 0x%04x, 0x%04x, 0x%04x, 0x%04x\n", i,
		       cyber5k_inw(c5k, i), cyber5k_inw(c5k, i + 2),
		       cyber5k_inw(c5k, i + 4), cyber5k_inw(c5k, i + 6));
}

/**
 * Set registers to a sane state.
 */
static void
cyber5k_reset_chip(struct cyber5k_info *c5k)
{
	cyber5k_ramdac_off(c5k);

	/* Reset switch registers. */
	cyber5k_outw(c5k, 0x3ce, MKW(0x33, 0x01));
	cyber5k_outw(c5k, 0x3ce, MKW(0x56, 0x00));
	cyber5k_outw(c5k, 0x3ce, MKW(0xbf, 0x00));
	cyber5k_outw(c5k, 0x3ce, MKW(0xfa, 0x00));

	/* Force external RC filter. */
	cyber5k_outw(c5k, 0x3ce, MKW(0xbf, 0x03));
	cyber5k_outw(c5k, 0x3ce, MKW(0xb2, 0x00));
	cyber5k_outw(c5k, 0x3ce, MKW(0xb5, 0x00));
	cyber5k_outw(c5k, 0x3ce, MKW(0xbf, 0x00));

	/* Unlock crt registers. */
	cyber5k_outb(c5k, 0x3c2, cyber5k_inb(c5k, 0x3cc) | 0x01);
	cyber5k_outw(c5k, 0x3d4, MKW(0x1f, 0x03));

	/* Reset underscan registers. */
	cyber5k_outw(c5k, 0x3d4, MKW(0x44, 0x00));
	cyber5k_outw(c5k, 0x3d4, MKW(0x45, 0x00));

	/* Reset video capture register. */
	cyber5k_outw(c5k, 0x3ce, MKW(0xe8, 0x00));

	/* Reset video overlay registers. */
	cyber5k_outw(c5k, 0x3ce, MKW(0xdc, 0x00));
	cyber5k_outw(c5k, 0x3c4, MKW(0xdc, 0x00));

	/* Reset alpha blending registers. */
	cyber5k_outw(c5k, 0x3c4, MKW(0x4b, 0x00));

	cyber5k_outw(c5k, 0x3ce, MKW(0x12,0x00));	/* crt irq control */
	cyber5k_outw(c5k, 0x3ce, MKW(0x16,0x00));	/* hsync/vsync ctrl */
	cyber5k_outw(c5k, 0x3ce, MKW(0x30,0xa6));	/* bus control */
	cyber5k_outw(c5k, 0x3ce, MKW(0x31,0x00));	/* segment write */
	cyber5k_outw(c5k, 0x3ce, MKW(0x32,0x00));	/* segment read */
	cyber5k_outw(c5k, 0x3ce, MKW(0x3c,0x00));	/* function control */
	cyber5k_outw(c5k, 0x3ce, MKW(0x3e,0x00));	/* BIU bus master */
	cyber5k_outw(c5k, 0x3ce, MKW(0x3f,0x00));	/* big endian swap */
	cyber5k_outw(c5k, 0x3ce, MKW(0x57,0x31));	/* attribute control */
	cyber5k_outw(c5k, 0x3ce, MKW(0x58,0x00));	/* overscan red */
	cyber5k_outw(c5k, 0x3ce, MKW(0x59,0x00));	/* overscan green */
	cyber5k_outw(c5k, 0x3ce, MKW(0x5a,0x00));	/* overscan blue */
	cyber5k_outw(c5k, 0x3ce, MKW(0x5c,0x00));	/* ramdac control */
	cyber5k_outw(c5k, 0x3ce, MKW(0x5d,0x00));	/* itp control 0 */
	cyber5k_outw(c5k, 0x3ce, MKW(0x5e,0x00));	/* itp control 1 */
	cyber5k_outw(c5k, 0x3ce, MKW(0x5f,0x00));	/* itp control 2 */
	cyber5k_outw(c5k, 0x3ce, MKW(0x73,0x10));	/* hidden control */
	cyber5k_outw(c5k, 0x3ce, MKW(0x76,0x00));	/* seq. programmable */
	cyber5k_outw(c5k, 0x3ce, MKW(0x78,0x00));	/* hidden control 2 */
	cyber5k_outw(c5k, 0x3ce, MKW(0x7a,0xc8));	/* hidden control 4 */
	cyber5k_outw(c5k, 0x3ce, MKW(0x9e,0x00));	/* misc graphics ctrl*/
	cyber5k_outw(c5k, 0x3ce, MKW(0xb5,0x80));	/* bx misc 0 */

	cyber5k_outw(c5k, 0x3ce, MKW(0xbf,0x01));
	cyber5k_outw(c5k, 0x3ce, MKW(0xb8,0x02));	/* dll sclk bypass */
	cyber5k_outw(c5k, 0x3ce, MKW(0xbf,0x00));

	/* Kick memory. */
	cyber5k_reset_ram(c5k);

	/* Initialize hardware cursor. */
	cyber5k_cursor_init(c5k);

	cyber5k_orx(c5k, 0x3ce, 0x56, 0x04);	/* switch to cursor 3c6-3c9 */
	/* ramdac control: dac power on, voltage reference enable, 8 bit DAC */
	cyber5k_outb(c5k, 0x3c6, 0x46);
	cyber5k_andx(c5k, 0x3ce, 0x56, 0xfb);	/* switch to vga 3c6-3c9 */
}


/**
 * Black magic. Sometimes the TV shows strangly inverted colors.
 * This functions corrects this. Called with 3cf/fa = 0x05
 */
static void
cyber5k_correct_tvcolor(struct cyber5k_info *c5k, struct cyber5k_regs *regs)
{
	unsigned short color_out;

	cyber5k_orx(c5k, 0x3ce, 0x4e, 0x02);
	cyber5k_outw(c5k, 0xbe430, regs->tv_regs[2]|3);	/* TV off. */
	cyber5k_outw(c5k, 0xbe440, 0x0020);		/* Set small screen */
	cyber5k_outw(c5k, 0xbe448, 0x0020);
	udelay(10000);
	cyber5k_outl(c5k, 0xbe604, 0x0000ff40);		/* Show blue test. */
	cyber5k_vsync_wait(c5k);
	cyber5k_vsync_wait(c5k);
	udelay(10000);
	/* Read color u, v detection value. */
	color_out = cyber5k_inw(c5k, 0xbe454);
	cyber5k_outw(c5k, 0xbe604, 0x00000000);		/* Reset to normal. */
	cyber5k_outw(c5k, 0xbe440, regs->tv_regs[6]);	/* Reset size. */
	cyber5k_outw(c5k, 0xbe448, regs->tv_regs[8]);
	cyber5k_outw(c5k, 0xbe430, regs->tv_regs[2]);	/* TV on. */
	cyber5k_andx(c5k, 0x3ce, 0x4e, 0xfd);
	/* Check color if a toggle is necessary. */
	if ((color_out & 100) == 0)
		/* Toggle color through 0xbe46c bit 0x01. */
		cyber5k_outw(c5k, 0xbe46c,
			     cyber5k_inw(c5k, 0xbe46c) ^ 0x0001);
}

static void
cyber5k_program_tv(struct cyber5k_info *c5k, struct cyber5k_regs *regs)
{
	unsigned short *words;
	int i;

	cyber5k_outw(c5k, 0x3ce, MKW(0xfa, 0x05));
	if (regs->tv_regs) {
		words = regs->tv_regs;
		for (i = 0; i < 66; i++)
			cyber5k_outw(c5k, 0xbe428 + 4*i, words[i]);
		for (i = 0; i < 34; i++)
			cyber5k_outw(c5k, 0xbe530 + 2*i, words[66+i]);
		for (i = 0; i < 84; i++)
			cyber5k_outw(c5k, 0xbe574 + 2*i, words[100+i]);
		/**
		 * 3cf/fa05.3cf/4e
		 *   bit 7: scart tv sync
		 *   bit 6: scart connector output on tv dac
		 *   bit 5: blank rgb dac
		 *   bit 4: scart connector on
		 *   bit 3: uv color flip
		 *   bit 2: tv enable
		 *   bit 1: reserved
		 *   bit 0: v mux flip (pal vs. ntsc)
		 * FIXME: scart ?
		 */
		if (regs->tvnorm == 0)
			cyber5k_outx(c5k, 0x3ce, 0x4e, 0x0d);	/* pal */
		else
			cyber5k_outx(c5k, 0x3ce, 0x4e, 0x0c);	/* ntsc */
		cyber5k_orx(c5k, 0x3ce, 0x5c, 0x20);

		cyber5k_correct_tvcolor(c5k, regs);
	} else {
		cyber5k_outw(c5k, 0xbe430,
			     cyber5k_inw(c5k, 0xbe430) & 0xfefd);
		cyber5k_outx(c5k, 0x3ce, 0x4e, 0x00);
		cyber5k_andx(c5k, 0x3ce, 0x5c, 0xdf);
	}
	cyber5k_outw(c5k, 0x3ce, MKW(0xfa, 0x00));
}

static void
cyber5k_set_timing(struct fb_info *info,
		   struct cyber5k_info *c5k, struct cyber5k_regs *regs)
{
	static unsigned char palette[42] = {
		0x00, 0x00, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03,
		0x04, 0x04, 0x05, 0x05, 0x06, 0x06, 0x07, 0x07,
		0x08, 0x08, 0x09, 0x09, 0x0a, 0x0a, 0x0b, 0x0b,
		0x0c, 0x0c, 0x0d, 0x0d, 0x0e, 0x0e, 0x0f, 0x0f,
		0x10, 0x01, 0x11, 0x00, 0x12, 0x0f, 0x13, 0x00,
		0x14, 0x00
	};
	unsigned char seq_misc, cp_format;
	unsigned int log_width, mem_fetch;
	int i;

	switch (info->var.bits_per_pixel) {
	case 8:
		cp_format = 0;
		seq_misc = 0x01;	/* 8bit indexed */
		log_width = info->var.xres_virtual >> 3;
		break;
	case 16:
		cp_format = 1;
		if (info->var.green.length == 4)
			seq_misc = 0x0a;	/* rgb 4/4/4 */
		else if (info->var.green.length == 5)
			seq_misc = 0x06;	/* rgb 5/5/5 */
		else
			seq_misc = 0x02;	/* rgb 5/6/5 */
		log_width = (info->var.xres_virtual >> 3) * 2;
		break;
	case 24:
		cp_format = 2;
		seq_misc = 0x04;	/* rgb 8/8/8 */
		log_width = (info->var.xres_virtual >> 3) * 3;
		break;
	case 32:
		cp_format = 3;
		seq_misc = 0x03;	/* transp. 8 + rgb 8/8/8 */
		log_width = (info->var.xres_virtual >> 3) * 4;
		break;
	default:
		return;
	}
	mem_fetch = log_width + 1;

	for (i = 0; i < NR_PALETTE; i++) {
		cyber5k_outb(c5k, 0x3c8, i);
		cyber5k_outb(c5k, 0x3c9, 0);
		cyber5k_outb(c5k, 0x3c9, 0);
		cyber5k_outb(c5k, 0x3c9, 0);
	}

	cyber5k_ramdac_off(c5k);

	cyber5k_outw(c5k, 0x3c4, MKW(0x00, 0x00));
	cyber5k_outw(c5k, 0x3c4, MKW(0x01, 0x01));
	cyber5k_outw(c5k, 0x3c4, MKW(0x02, 0x0f));
	cyber5k_outw(c5k, 0x3c4, MKW(0x03, 0x00));
	cyber5k_outw(c5k, 0x3c4, MKW(0x04, 0x0e));
	cyber5k_outb(c5k, 0x3c2, 0xef);
	cyber5k_outw(c5k, 0x3c4, MKW(0x00, 0x03));
	cyber5k_outw(c5k, 0x3d4, MKW(0x11, 0x00));
	cyber5k_inb(c5k, 0x3da);
	for (i = 0; i < 42; i++)
		cyber5k_outb(c5k, 0x3c0, palette[i]);
	cyber5k_outb(c5k, 0x3c0, 0x20);
	cyber5k_inb(c5k, 0x3da);
	cyber5k_outw(c5k, 0x3ce, MKW(0x00, 0x00));
	cyber5k_outw(c5k, 0x3ce, MKW(0x01, 0x00));
	cyber5k_outw(c5k, 0x3ce, MKW(0x02, 0x00));
	cyber5k_outw(c5k, 0x3ce, MKW(0x03, 0x00));
	cyber5k_outw(c5k, 0x3ce, MKW(0x04, 0x00));
	cyber5k_outw(c5k, 0x3ce, MKW(0x05, 0x60));
	cyber5k_outw(c5k, 0x3ce, MKW(0x06, 0x05));
	cyber5k_outw(c5k, 0x3ce, MKW(0x07, 0x0f));
	cyber5k_outw(c5k, 0x3ce, MKW(0x08, 0xff));

	cyber5k_outx(c5k, 0x3d4, 0x13, (unsigned char) log_width);
	cyber5k_outx(c5k, 0x3ce, 0x14, (unsigned char) mem_fetch);
	cyber5k_outx(c5k, 0x3ce, 0x15,
		     (unsigned char) ((log_width & 0x300) >> 4) |
		     (unsigned char) ((mem_fetch & 0x300) >> 8));
	cyber5k_outx(c5k, 0x3ce, 0xb0, regs->vclk[0]);
	cyber5k_outx(c5k, 0x3ce, 0xb1, regs->vclk[1]);
	cyber5k_outx(c5k, 0x3ce, 0xba, regs->vclk[2]);
	cyber5k_outx(c5k, 0x3ce, 0x77, seq_misc);

	for (i = 0; i <= 9; i++)
		cyber5k_outx(c5k, 0x3d4, i, regs->crtc[i]);
	for (i = 0; i <= 8; i++)
		cyber5k_outx(c5k, 0x3d4, 0x10 + i, regs->crtc[10 + i]);
	cyber5k_outx(c5k, 0x3ce, 0x11, regs->crtc[19]);

	cyber5k_toggle_clock(c5k);

	if (info->var.bits_per_pixel < 24) {
		/* Set up RGB to use ram-table. */
		cyber5k_orx(c5k, 0x3ce, 0x56, 0x04);
		cyber5k_andb(c5k, 0x3c6, 0xee);
		cyber5k_andx(c5k, 0x3ce, 0x56, 0xfb);
		cyber5k_outb(c5k, 0x3c8, 0);
		for (i = 0; i < 0x300; i++)
			cyber5k_outb(c5k, 0x3c9, 0);
		cyber5k_outb(c5k, 0x3c6, 0xff);
	} else {
		/* Set up RGB to bypass ram-table. */
		cyber5k_orx(c5k, 0x3ce, 0x56, 0x04);
		cyber5k_orb(c5k, 0x3c6, 0x11);
		cyber5k_andx(c5k, 0x3ce, 0x56, 0xfb);
	}

	cyber5k_program_tv(c5k, regs);

	/* Set up coprocessor registers. */
	cyber5k_outw(c5k, 0xbf018, info->var.xres_virtual - 1);
	cyber5k_outw(c5k, 0xbf218, info->var.xres_virtual - 1);
	cyber5k_outb(c5k, 0xbf01c, cp_format);

	/* All set, switch on ramdac. */
	cyber5k_ramdac_on(c5k);
}

static int
cyber5k_update_start(struct cyber5k_info *c5k, struct fb_var_screeninfo *var)
{
	unsigned int base = var->yoffset * var->xres_virtual + var->xoffset;
	base *= var->bits_per_pixel;

	/*
	 * Convert to bytes and shift two extra bits because DAC
	 * can only start on 4 byte aligned data.
	 */
	base >>= 5;
	if (base >= 1 << 20)
		return -EINVAL;

	cyber5k_outx(c5k, 0x3ce, 0x10, base >> 16 | 0x10);
	cyber5k_outx(c5k, 0x3d4, 0x0c, base >> 8);
	cyber5k_outx(c5k, 0x3d4, 0x0d, base);
	return 0;
}

/**
 * Calculate vertical sync frequency.
 */
static int
cyber5k_calc_vsync(struct fb_var_screeninfo *var)
{
	unsigned int htotal, vtotal, hsync, vsync;

	htotal = var->xres + var->left_margin +
		var->right_margin + var->hsync_len;
	vtotal = var->yres + var->upper_margin +
		var->lower_margin + var->vsync_len;
	/*
	 * hsync = 1e12 / pixclock / htotal.
	 * vsync = hsync / vtotal.
	 *
	 * 1e12 > 2^32, so use 1e12 = 1953125000 * 512 to avoid
	 * long long divisions.
	 */
	hsync = ((1953125000 + (var->pixclock >> 1)) / var->pixclock);
	vsync = ((hsync * 512) / htotal) / vtotal;
	return vsync;
}

/**
 * Calculate pll bytes for vga modes.
 */
static int
cyber5k_decode_pll(unsigned long ref_ps, unsigned long pll_ps,
		   unsigned char *pll_bytes)
{
	unsigned int mmd, mnd, mpd;
	unsigned long diff, best_diff;
	unsigned long t1, t2, t3;
	unsigned int mult, div, p;
	unsigned int low, high;

	mmd = mnd = mpd = 0;
	best_diff = 0x7fffffff;
	for (div = 2; div <= 255; div++) {
		t1 = ref_ps * div;
		for (p = 0; p <= 4; p++) {
			t2 = pll_ps / (p ? p*2 : 1);
			if (t2 >= 8695 || t2 <= 3846)
				continue;
			t2 = t1 * (p ? p*2 : 1);
			if (t2 < pll_ps >> 1)
				continue;
			low = (t2 - (pll_ps >> 1)) / pll_ps;
			high = (t2 + (pll_ps >> 2)) / pll_ps;
			if (high < 4 || low > 0x7ff)
				continue;
			if (low < 4)
				low = 4;
			if (high > 0x7ff)
				high = 0x7ff;
			for (mult = low; mult <= high; mult++) {
				t3 = t2 / mult;
				diff = pll_ps - t3;
				if (diff < 0)
					diff = -diff;
				if (diff < best_diff) {
					mmd = mult - 1;
					mnd = div - 1;
					mpd = p;
					best_diff = diff;
				}
			}
		}
	}
	if (best_diff > 100)
		return -EINVAL;

	pll_bytes[0] = (unsigned char) mmd;
	pll_bytes[1] = (unsigned char) ((mnd & 0x1f) +  (mpd << 6));
	pll_bytes[2] = 0x80 |
		((mpd & 0x4) << 4) | ((mnd & 0xe0) >> 2) | (mmd >> 8);

	t1 = ref_ps * (mmd + 1) / (mnd + 1) / (mpd ? mpd * 1 : 1);
	if ((ref_ps == 40690) &&  t1 < 5556)
		pll_bytes[2] |= 0x20;

	return 0;
}

/**
 * Decode crt registers from var for vga modes.
 * regs->crtc[0] .. regs->crtc[9] are 0x3d5/0x00 .. 0x3d5/0x09
 * regs->crtc[10] .. regs->crtc[18] are 0x3d5/0x10 .. 0x3d5/0x18
 * regs->crtc[19] is 0x3cf/0x11
 */
static int
cyber5k_decode_crtc(struct fb_var_screeninfo *var, unsigned char *crtc)
{
	unsigned int total, blankstart, blankend, syncstart, syncend;

	/**
	 * Horizontal timing.
	 */
	total = var->xres + var->left_margin +
		var->right_margin + var->hsync_len;
	if (total > 2080)
		return -EINVAL;
	blankstart = var->xres >> 3;
	blankend = (total >> 3) - 4;
	syncstart = (var->xres + var->right_margin) >> 3;
	syncend = (total - var->left_margin) >> 3;

	crtc[0] = (unsigned char) ((total >> 3) - 5);
	crtc[1] = (unsigned char) (blankstart - 1);
	crtc[2] = (unsigned char) blankstart;
	crtc[3] = (unsigned char) (blankend & 0x1f) | 0x80;
	crtc[4] = (unsigned char) syncstart;
	crtc[5] = (unsigned char) (syncend & 0x1f) |
		  (unsigned char) ((blankend & 0x20) << 2);

	/**
	 * Vertical timing.
	 */
	total = var->yres + var->upper_margin + 
		var->lower_margin + var->vsync_len;
	if (total > 2047)
		return -EINVAL;
	blankstart = var->yres + 6;
	blankend  = total - 12;
	syncstart = var->yres + var->lower_margin;
	syncend = total - var->upper_margin;
	crtc[6] = (unsigned char) (total - 2);
	crtc[7] = (unsigned char) (((total - 2) & 0x100) >> 8) |
		  (unsigned char) (((var->yres - 1) & 0x100) >> 7) |
		  (unsigned char) ((syncstart & 0x100) >> 6) |
		  (unsigned char) ((blankstart & 0x100) >> 5) |
		  0x10 |
		  (unsigned char) (((total - 2) & 0x200) >> 4) |
		  (unsigned char) (((var->yres - 1) & 0x200) >> 3) |
		  (unsigned char) ((syncstart & 0x200) >> 2);
	crtc[8] = 0;
	crtc[9] = (unsigned char) ((blankstart & 0x200) >> 4) | 0x40;
	crtc[10] = (unsigned char) syncstart;
	crtc[11] = (unsigned char) (syncend & 0x0f) | 0x80;
	crtc[12] = (unsigned char) (var->yres - 1);
	crtc[13] = (var->xres_virtual * (var->bits_per_pixel >> 3)) >> 3;
	crtc[14] = 0;
	crtc[15] = (unsigned char) blankstart;
	crtc[16] = (unsigned char) blankend;
	crtc[17] = 0xe3;
	crtc[18] = 0xff;
	crtc[19] = (unsigned char) (((total - 2) & 0x400) >> 10) |
		   (unsigned char) (((var->yres - 1) & 0x400) >> 9) |
		   (unsigned char) ((syncstart & 0x400) >> 8) |
		   (unsigned char) ((blankstart & 0x400) >> 7) |
		   0x10;
	if ((var->vmode & FB_VMODE_MASK) == FB_VMODE_INTERLACED)
		crtc[19] |= 0x20;
	return 0;
}

/**
 * Find a tv mode that can be used for given resolution. This ignores all
 * fields in the screen info var except xres and yres. All tv modes use
 * fixed 50 HZ refresh rate.
 * @var: frame buffer variable screen structure.
 * @regs: structure keeping the hardware specific information for a timing
 */
static int
cyber5k_find_tvmode(struct fb_var_screeninfo *var, struct cyber5k_regs *regs)
{
	int i;

	regs->tv_regs = NULL;
	for (i = 0; cyber5k_tv_modes[i].xres > 0; i++) {
		if (var->xres == cyber5k_tv_modes[i].xres &&
		    var->yres == cyber5k_tv_modes[i].yres) {
			regs->tv_regs = cyber5k_tv_modes[i].tv_regs;
			memcpy(regs->vclk, cyber5k_tv_modes[i].vclk, 3);
			memcpy(regs->crtc, cyber5k_tv_modes[i].crtc, 20);
			regs->crtc[13] = (var->xres_virtual *
					(var->bits_per_pixel >> 3)) >> 3;
			regs->tvnorm = cyber5k_tv_modes == cyber5k_ntsc_modes;
			return 0;
		}
	}
	return -EINVAL;
}

/**
 * Validate a screen timing.
 * @var: frame buffer variable screen structure.
 * @info: frame buffer structure that represents a single frame buffer.
 */
static int
cyber5k_check_var(struct fb_var_screeninfo *var, struct fb_info *info)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;
	struct cyber5k_regs regs;
	unsigned int line_length;
	int rc;

	var->transp.msb_right = 0;
	var->red.msb_right = 0;
	var->green.msb_right = 0;
	var->blue.msb_right = 0;

	switch (var->bits_per_pixel) {
	case 8:		/* pseudocolor, 256 */
		var->transp.offset = 0;
		var->transp.length = 0;
		var->red.offset = 0;
		var->red.length = 8;
		var->green.offset = 0;
		var->green.length = 8;
		var->blue.offset = 0;
		var->blue.length = 8;
		break;
	case 16:	/* directcolor, 64k or 32k */
		switch (var->green.length) {
		case 6: /* rgb 565, 64k */
			var->transp.offset	= 0;
			var->transp.length	= 0;
			var->red.offset		= 11;
			var->red.length		= 5;
			var->green.offset	= 5;
			var->green.length	= 6;
			var->blue.offset	= 0;
			var->blue.length	= 5;
			break;
		default:
		case 5: /* rgb 555, 32k */
			var->transp.offset	= 0;
			var->transp.length	= 0;
			var->red.offset		= 10;
			var->red.length		= 5;
			var->green.offset	= 5;
			var->green.length	= 5;
			var->blue.offset	= 0;
			var->blue.length	= 5;
			break;

		case 4: /* rgb 444, 4k + transparency? */
			var->transp.offset	= 12;
			var->transp.length	= 4;
			var->red.offset		= 8;
			var->red.length		= 4;
			var->green.offset	= 4;
			var->green.length	= 4;
			var->blue.offset	= 0;
			var->blue.length	= 4;
			break;
		}
		break;
	case 24:	/* truecolor, 16m */
		var->transp.offset	= 0;
		var->transp.length	= 0;
		var->red.offset		= 16;
		var->red.length		= 8;
		var->green.offset	= 8;
		var->green.length	= 8;
		var->blue.offset	= 0;
		var->blue.length	= 8;
		break;
	case 32:	/* truecolor, 16m */
		var->transp.offset	= 24;
		var->transp.length	= 8;
		var->red.offset		= 16;
		var->red.length		= 8;
		var->green.offset	= 8;
		var->green.length	= 8;
		var->blue.offset	= 0;
		var->blue.length	= 8;
		break;
	default:
		return -EINVAL;
	}

	line_length = var->xres_virtual * (var->bits_per_pixel / 8);
	if (var->yres_virtual * line_length > info->fix.smem_len)
		var->yres_virtual = info->fix.smem_len / line_length;
	if (var->yres > var->yres_virtual)
		var->yres = var->yres_virtual;
	if (var->xres > var->xres_virtual)
		var->xres = var->xres_virtual;

	if (!tvmode) {
		regs.tv_regs = NULL;
		rc = cyber5k_decode_pll(c5k->ref_ps, var->pixclock,
					&regs.vclk[0]);
		if (rc)
			return rc;
		rc = cyber5k_decode_crtc(var, &regs.crtc[0]);
	} else
		rc = cyber5k_find_tvmode(var, &regs);
	return rc;
}

/**
 * (Un)Blank the display.
 *    blank == 0: unblank
 *    blank == 1: soft blank
 *    blank == 2: suspend vsync
 *    blank == 3: suspend hsync
 *    blank == 4: powerdown
 */
static int
cyber5k_blank(int blank, struct fb_info *info)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;
	int i;

	/* Turn off/on the hardware cursor */
	c5k->blanked = blank;
	if (c5k->cursor_on) {
		if (blank)
			cyber5k_cursor_hide(c5k);
		else
			cyber5k_cursor_show(c5k);
	}

	switch (blank) {
	case 4: /* powerdown - both sync lines down */
		cyber5k_outw(c5k, 0x3ce, MKW(0x16, 0x05));
		break;
	case 3: /* hsync off */
		cyber5k_outw(c5k, 0x3ce, MKW(0x16, 0x01));
		break;
	case 2: /* vsync off */
		cyber5k_outw(c5k, 0x3ce, MKW(0x16, 0x04));
		break;
	case 1: /* soft blank */
		cyber5k_outw(c5k, 0x3ce, MKW(0x16, 0x00));
		for (i = 0; i < NR_PALETTE; i++) {
			cyber5k_outb(c5k, 0x3c8, i);
			cyber5k_outb(c5k, 0x3c9, 0);
			cyber5k_outb(c5k, 0x3c9, 0);
			cyber5k_outb(c5k, 0x3c9, 0);
		}
		break;
	default: /* unblank */
		cyber5k_outw(c5k, 0x3ce, MKW(0x16, 0x00));
		for (i = 0; i < NR_PALETTE; i++) {
			cyber5k_outb(c5k, 0x3c8, i);
			cyber5k_outb(c5k, 0x3c9, c5k->palette[i].red);
			cyber5k_outb(c5k, 0x3c9, c5k->palette[i].green);
			cyber5k_outb(c5k, 0x3c9, c5k->palette[i].blue);
		}
		break;
	}
	if (blank == 0)
		cyber5k_ramdac_on(c5k);
	else
		cyber5k_ramdac_off(c5k);
	return 0;
}

/**
 * Alter the hardware state to the timing in info->var.
 * @info: frame buffer structure that represents a single frame buffer
 */
static int
cyber5k_set_par(struct fb_info *info)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;
	struct fb_var_screeninfo *var = &info->var;
	struct cyber5k_regs regs;

	if (var->bits_per_pixel == 8)
		info->fix.visual = FB_VISUAL_PSEUDOCOLOR;
	else if (var->bits_per_pixel == 24)
		info->fix.visual = FB_VISUAL_TRUECOLOR;
	else
		info->fix.visual = FB_VISUAL_DIRECTCOLOR;
	info->fix.line_length = var->xres_virtual * (var->bits_per_pixel/8);

	if (!tvmode) {
		cyber5k_decode_pll(c5k->ref_ps, var->pixclock, &regs.vclk[0]);
		cyber5k_decode_crtc(var, &regs.crtc[0]);
	} else
		cyber5k_find_tvmode(var, &regs);

	cyber5k_reset_chip(c5k);
	cyber5k_update_start(c5k, var);
	cyber5k_set_timing(info, c5k, &regs);

	printk(KERN_INFO "cyber5k: mode = %dx%d-%d@%d\n", var->xres, var->yres,
	       var->bits_per_pixel, cyber5k_calc_vsync(var));

	return 0;
}

/**
 * Pans the display.
 * @var: frame buffer variable screen structure
 * @info: frame buffer structure that represents a single frame buffer
 */
static int
cyber5k_pan_display(struct fb_var_screeninfo *var, struct fb_info *info)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;

	if (cyber5k_update_start(c5k, var))
		return -EINVAL;

	info->var.xoffset = var->xoffset;
	info->var.yoffset = var->yoffset;
	if (var->vmode & FB_VMODE_YWRAP)
		info->var.vmode |= FB_VMODE_YWRAP;
	else
		info->var.vmode &= ~FB_VMODE_YWRAP;
	return 0;
}

static inline unsigned int
convert_bitfield(unsigned int val, struct fb_bitfield *bf)
{
	unsigned int mask = (1 << bf->length) - 1;
	return (val >> (8 - bf->length) & mask) << bf->offset;
}

/**
 * Set a color register.
 * @nr: Which register in the CLUT we are programming
 * @red: The red value which can be up to 16 bits wide
 * @green: The green value which can be up to 16 bits wide
 * @blue:  The blue value which can be up to 16 bits wide.
 * @transp: If supported the alpha value which can be up to 16 bits wide.
 * @info: frame buffer info structure
 */
static int
cyber5k_setcolreg(unsigned int nr, unsigned int red, unsigned int green,
		  unsigned blue, unsigned int transp, struct fb_info *info)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;
	struct fb_var_screeninfo *var = &info->var;
	unsigned int pseudo_val;
	int rc;

	/* grayscale works only partially under directcolor */
	if (info->var.grayscale)
		/* grayscale = 0.30*R + 0.59*G + 0.11*B */
		red = green = blue = (red * 77 + green * 151 + blue * 28) >> 8;

	red >>= 8;
	green >>= 8;
	blue >>= 8;

	if (info->fix.visual == FB_VISUAL_PSEUDOCOLOR) {
		if (nr >= NR_PALETTE)
			return 1;
		c5k->palette[nr].red = red;
		c5k->palette[nr].green = green;
		c5k->palette[nr].blue = blue;
		cyber5k_outb(c5k, 0x3c8, nr);
		cyber5k_outb(c5k, 0x3c9, red);
		cyber5k_outb(c5k, 0x3c9, green);
		cyber5k_outb(c5k, 0x3c9, blue);
		return 0;
	}
	if (info->fix.visual == FB_VISUAL_TRUECOLOR) {
		if (nr >= 16)
			return 1;
		pseudo_val = convert_bitfield(transp ^ 0xff, &var->transp);
		pseudo_val |= convert_bitfield(red, &var->red);
		pseudo_val |= convert_bitfield(green, &var->green);
		pseudo_val |= convert_bitfield(blue, &var->blue);
		c5k->pseudo_palette[nr] = pseudo_val;
		return 0;
	}
	/* info->fix.visual == FB_VISUAL_DIRECTCOLOR */
	rc = 1;
	if (info->var.green.length == 6 && nr < 64) {
		c5k->palette[nr << 2].green = green;
		cyber5k_outb(c5k, 0x3c8, nr << 2);
		cyber5k_outb(c5k, 0x3c9, c5k->palette[nr >> 1].red);
		cyber5k_outb(c5k, 0x3c9, c5k->palette[nr >> 1].blue);
		green = c5k->palette[nr << 3].green;
		rc = 0;
	}
	if (info->var.green.length >= 5 && nr < 32) {	/* >= 5 !! */
		c5k->palette[nr << 3].red = red;
		c5k->palette[nr << 3].green = green;
		c5k->palette[nr << 3].blue = blue;
		cyber5k_outb(c5k, 0x3c8, nr << 3);
		cyber5k_outb(c5k, 0x3c9, red);
		cyber5k_outb(c5k, 0x3c9, green);
		cyber5k_outb(c5k, 0x3c9, blue);
		rc = 0;
	}
	if (info->var.green.length == 4 && nr < 16) {
		c5k->palette[nr << 4].red = red;
		c5k->palette[nr << 4].green = green;
		c5k->palette[nr << 4].blue = blue;
		cyber5k_outb(c5k, 0x3c8, nr << 4);
		cyber5k_outb(c5k, 0x3c9, red);
		cyber5k_outb(c5k, 0x3c9, green);
		cyber5k_outb(c5k, 0x3c9, blue);
		rc = 0;
	}
	if (nr < 16)
		c5k->pseudo_palette[nr] = nr << info->var.red.offset |
					  nr << info->var.green.offset |
					  nr << info->var.blue.offset;
	return rc;
}

/**
 * Framebuffer operations.
 */
static struct fb_ops cyber5k_ops = {
	.owner		= THIS_MODULE,
	.fb_check_var	= cyber5k_check_var,
	.fb_set_par	= cyber5k_set_par,
	.fb_setcolreg	= cyber5k_setcolreg,
	.fb_blank	= cyber5k_blank,
	.fb_pan_display	= cyber5k_pan_display,
	.fb_fillrect	= cyber5k_fillrect,
	.fb_copyarea	= cyber5k_copyarea,
	.fb_imageblit	= cfb_imageblit,	// FIXME
	.fb_cursor	= cyber5k_cursor,
	.fb_sync	= cyber5k_sync,
};

/**
 * c5k allocation/free functions.
 */
static struct fb_info *
cyber5k_alloc_info(struct pci_dev *dev, const struct pci_device_id *id)
{
	struct cyber5k_info *c5k;
	struct fb_info *info;

	info = framebuffer_alloc(sizeof(struct cyber5k_info), &dev->dev);
	if (!info)
		return NULL;
	c5k = (struct cyber5k_info *) info->par;
	memset(c5k, 0, sizeof(struct cyber5k_info));
	c5k->palette = (struct cyber5k_color *)
		kmalloc(sizeof(struct cyber5k_color)*NR_PALETTE, GFP_KERNEL);
	if (!c5k->palette) {
		framebuffer_release(info);
		return NULL;
	}

	sprintf(info->fix.id, "CyberPro%4X", id->device);
	c5k->dev = dev;

	/**
	 * Set reference clock frequency. No way to detect this, for
	 * the activy 300 this is 14.31818 MHz. Other machines vary.
	 *
	 * 40690 = 24.576 MHz, 69842 = 14.31818 MHz, 81380 = 12.288 MHz
	 */
	c5k->ref_ps = 69842;

	info->fix.type = FB_TYPE_PACKED_PIXELS;
	info->fix.type_aux = 0;
	info->fix.visual = FB_VISUAL_PSEUDOCOLOR;
	info->fix.xpanstep = 0;
	info->fix.ypanstep = 1;
	info->fix.ywrapstep = 0;
	info->fix.accel = FB_ACCEL_IGS_CYBER5000;
	info->var.nonstd = 0;
	info->var.activate = FB_ACTIVATE_NOW;
	info->var.height = -1;
	info->var.width = -1;
	if (accel)
		info->var.accel_flags |= FB_ACCELF_TEXT;
	else
		info->var.accel_flags &= ~FB_ACCELF_TEXT;
	info->var.accel_flags = 0;
	info->fbops = &cyber5k_ops;
	info->flags = FBINFO_FLAG_DEFAULT | FBINFO_HWACCEL_YPAN;
	info->pseudo_palette = c5k->pseudo_palette;

	fb_alloc_cmap(&info->cmap, NR_PALETTE, 0);

	return info;
}

static void __devinit
cyber5k_free_info(struct fb_info *info)
{
	struct cyber5k_info *c5k;

	if (!info)
		return;
	/* Allocating cmap to 0 entries frees it. */
	fb_alloc_cmap(&info->cmap, 0, 0);
	c5k = (struct cyber5k_info *) info->par;
	kfree(c5k->palette);
	framebuffer_release(info);
}


/**
 * Common probe function.
 */
static int __devinit
cyber5k_common_probe(struct fb_info *info)
{
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;
	int rc;

	/* Get memory size. */
	// FIXME: do memory detection
	c5k->vmem_size = 0x100000 << (cyber5k_inx(c5k, 0x3ce, 0x72) & 0x03);

	/* Use 1024 bytes at the end of the video memory for the cursor. */
	c5k->cursor_offset = c5k->vmem_size - 0x400;

	info->fix.smem_len   = c5k->vmem_size - 0x400;
	info->fix.mmio_len   = MMIO_SIZE;
	info->screen_base    = (unsigned char *) c5k->smem_base;

	if (tvnorm) {
		if (strncmp(tvnorm, "pal", 3) == 0)
			cyber5k_tv_modes = cyber5k_pal_modes;
		else if (strncmp(tvnorm, "ntsc", 4) == 0)
			cyber5k_tv_modes = cyber5k_ntsc_modes;
		else
			return -EINVAL;
	} else
		cyber5k_tv_modes = cyber5k_pal_modes;

	if (tvmode)
		fb_find_mode(&info->var, info, tvmode,
			     cyber5k_fake_videomodes, 10, NULL, 0);
	else
		fb_find_mode(&info->var, info, mode,
			     NULL, 0, &cyber5k_default_mode, 8);

	info->var.yres_virtual = info->fix.smem_len * 8 /
		(info->var.bits_per_pixel * info->var.xres_virtual);
	if (c5k->dev)
		info->device = &c5k->dev->dev;

	rc = register_framebuffer(info);
	if (rc)
		return rc;

	printk(KERN_INFO "cyber5kfb: %s %ld MB\n", info->fix.id,
	       c5k->vmem_size >> 20);
	return 0;
}

static void
cyber5k_common_resume(struct fb_info *info)
{
	/**
	 * Restore the currently active video mode and the palette.
	 * We also need to tell fbcon to redraw the console.
	 */
	cyber5k_set_par(info);
}

#ifdef DEBUG
/**
 * Include debugging code.
 */
#include "cyber5k_debug.c"
#endif

/**
 * PCI bus related stuff.
 */

/**
 * Enable access to the linear memory region and allow
 * access to memory mapped registers.
 */
static void
cyber5k_pci_enable_mmio(struct cyber5k_info *c5k)
{
	unsigned char val;
	/* Use the standard IO-based wakeup. */
	outb(0x18, 0x46e8);
	outb(0x01, 0x102);
	outb(0x08, 0x46e8);
	outb(0x33, 0x3ce);
	outb(0x01, 0x3cf);
	outb(0x1f, 0x3d4);
	outb(0x03, 0x3d5);

	/* Allow PCI burst accesses. */
	outb(0x30, 0x3ce);
	val = inb(0x3cf) | 0xa0;	/* PCI read & write bursts. */
	outb(val, 0x3cf);
}

/**
 * Try to talk to a device with an appropriate pci-id.
 */
static int __devinit
cyber5k_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
	struct fb_info *info;
	struct cyber5k_info *c5k;
	int rc;

	rc = pci_enable_device(dev);
	if (rc)
		goto out;

	rc = -ENOMEM;
	info = cyber5k_alloc_info(dev, id);
	if (!info)
		goto out_disable;
	c5k = (struct cyber5k_info *) info->par;

	/* Set base addresses. */
	c5k->base_phys = pci_resource_start(dev, 0);

	rc = -ENODEV;
	/* Request region. */
	if (!request_mem_region(c5k->base_phys, pci_resource_len(dev, 0),
				info->fix.id))
		goto out_free;

	/* Map region. */
	c5k->smem_base = (unsigned long)
		ioremap(c5k->base_phys, pci_resource_len(dev, 0));
	if (!c5k->smem_base)
		goto out_release;
	c5k->mmio_base = c5k->smem_base + MMIO_OFFSET;

	info->fix.smem_start = (unsigned long) c5k->base_phys;
	info->fix.mmio_start = (unsigned long) c5k->base_phys + MMIO_OFFSET;

	cyber5k_pci_enable_mmio(c5k);

#ifdef DEBUG
	/* Debugging */
	cyber5k_save_state(c5k);
	if (dump_regs)
		cyber5k_print_state(c5k);
#endif

	/* Print mode to syslog. */
	if (dump_mode)
		cyber5k_print_mode(c5k);

	/* Poke the graphics card. */
	rc = cyber5k_common_probe(info);
	if (rc)
		goto out_unmap;

	/* Decode memory pll registers. */
	// cyber5k_decode_pll(c5k->ref_ps, 1000000UL / mclk, &c5k->mclk[0]);
	// FIXME: why can the memory clock not be calculated ??.
#if 0
	c5k->mclk[0] = 0x70;
	c5k->mclk[1] = 0x7f;
	c5k->mclk[2] = 0x8a;
#else
       c5k->mclk[0] = cyber5k_inx(c5k, 0x3ce, 0xb2);
       c5k->mclk[1] = cyber5k_inx(c5k, 0x3ce, 0xb3);
       c5k->mclk[2] = cyber5k_inx(c5k, 0x3ce, 0xbb);
#endif

	
	pci_set_drvdata(dev, info);
	return 0;

out_unmap:
	iounmap((unsigned char *) c5k->smem_base);
out_release:
	release_mem_region (c5k->base_phys, pci_resource_len(dev, 0));
out_free:
	cyber5k_free_info(info);
out_disable:
	pci_disable_device(dev);
out:
	return rc;
	
}

/**
 * Remove kernel data structures after card has been ripped out.
 */
static void __devexit
cyber5k_pci_remove(struct pci_dev *dev)
{
	struct fb_info *info = pci_get_drvdata(dev);
	struct cyber5k_info *c5k = (struct cyber5k_info *) info->par;

	if (!info)
		return;
	pci_set_drvdata(dev, NULL);
	if (unregister_framebuffer(info))
		/* The unregister should better not fail. */
		printk(KERN_WARNING "%s: danger Will Robinson.\n",
		       info->fix.id);
#ifdef DEBUG
	cyber5k_restore_state(c5k);
#endif
	iounmap((unsigned char *) c5k->smem_base);
	release_mem_region(c5k->base_phys, pci_resource_len(dev, 0));
	cyber5k_free_info(info);
	pci_disable_device(dev);
}

/**
 * Nothing to do for suspend.
 */
static int
cyber5k_pci_suspend(struct pci_dev *dev, unsigned int state)
{
	return 0;
}

/**
 * Re-initialise the CyberPro hardware
 */
static int
cyber5k_pci_resume(struct pci_dev *dev)
{
	struct fb_info *info = pci_get_drvdata(dev);
	struct cyber5k_info *c5k;

	if (!info)
		return 0;
	c5k = (struct cyber5k_info *) info->par;
	if (!c5k)
		return 0;
	cyber5k_pci_enable_mmio(c5k);
	cyber5k_common_resume(info);

	return 0;
}

/**
 * PCI-ID table of supported devices.
 */
static struct pci_device_id cyber5k_pci_table[] = {
	{ PCI_VENDOR_ID_INTERG, PCI_DEVICE_ID_INTERG_5000,
	  PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0 },
};

MODULE_DEVICE_TABLE(pci,cyber5k_pci_table);

static struct pci_driver cyber5k_driver = {
	.name		= "Cyber5k",
	.probe		= cyber5k_pci_probe,
	.remove		= __devexit_p(cyber5k_pci_remove),
	.suspend	= cyber5k_pci_suspend,
	.resume		= cyber5k_pci_resume,
	.id_table	= cyber5k_pci_table
};

int __init
cyber5k_init(void)
{
	return pci_module_init(&cyber5k_driver);
}

static void __exit
cyber5k_exit(void)
{
	pci_unregister_driver(&cyber5k_driver);
}

module_init(cyber5k_init);
#ifdef MODULE
module_exit(cyber5k_exit);
#endif
