/*
 * drivers/media/video/vino.c
 *
 * (incomplete) Driver for the VINO (Video In No Out) system found in SGI Indys.
 *
 * Copyright (C) 1999 Ulf Carlsson <ulfc@bun.falkenberg.se>
 * Copyright (C) 2001 Ladislav Michl <ladis@psi.cz>
 * Copyright (C) 2001 Petter Reinholdtsen <pere@hungry.com>
 *
 * Documentation is available at
 * <ftp://oss.sgi.com/pub/linux/mips/doc/indy/vino/>
 */

#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/wrapper.h>
#include <linux/errno.h>
#include <linux/delay.h>
#include <linux/videodev.h>
#ifdef CONFIG_PROC_FS
#include <linux/proc_fs.h>
#endif

#include <asm/addrspace.h>
#include <asm/system.h>
#include <asm/io.h>
#include <asm/pgtable.h>
#include <asm/irq.h>
#include <asm/sgi/sgint23.h>
#include <asm/sgi/sgihpc.h>

#include "vino.h"

/* debugging? */
#if 1
#define DEBUG(x...)     printk(x);
#else
#define DEBUG(x...)
#endif

/* I2C debugging? */
#if 0
#define DEBUGI2C(x...)     printk(x);
#else
#define DEBUGI2C(x...)
#endif

static char *vinostr = "VINO IndyCam/TV";
static int video_nr = -1;
static int threshold_a = 512;
static int threshold_b = 512;

/* revision of VINO board */
static unsigned int board_rev;
/* version of IndyCam (-1 if not connected) */
static int camera_version;
/* Phillips analog inputs status */
static unsigned int analog_status;

struct vino_device {
	struct video_device vdev;
	struct semaphore lock;
#define VINO_CHAN_A		0
#define VINO_CHAN_B		1
	int chan;
	int alpha;
	/* clipping... */
	unsigned int left, right, top, bottom;
	/* decimation used */
	unsigned int decimation;
	wait_queue_head_t wait;
	volatile int frame_ready;
	/* Make sure we only have one user at the time */
	int users;
#define VINO_DMA_ACTIVE         (1<<0)
	unsigned int status;
	/* palette used... */
	unsigned int palette;
	/* VINO_INPUT_COMPOSITE, VINO_INPUT_SVIDEO or VINO_INPUT_CAMERA */
	unsigned int input;
	/* bytes per line */
	unsigned int line_size;
	/* load this into VINO descriptor registers... */
	unsigned long *dummy_buf;
	/* pointer to array of allocated pages */
	unsigned long *pages;
	/* # of allocated pages */
	int page_count;
	/* physical addresses of allocated pages */
	unsigned long *phys_pages;
#define VINO_MAX_WIDTH		768
#define VINO_MAX_HEIGHT		576
#define VINO_FBUFSIZE		(VINO_MAX_WIDTH * VINO_MAX_HEIGHT * 4)
	unsigned long frame_size;
	int frames;
	/* index from VINO_BASE for current channel */
	unsigned long base;
};

/* We can actually receive TV and IndyCam input at the same time. Believe it 
 * or not... */
#define CHANNELS 2
static struct vino_device Vino[CHANNELS];

typedef struct {
	unsigned int reg_addr;
	unsigned int default_value;
	unsigned char rw;
} i2c_indycam_registers;

typedef struct {
	unsigned int reg_addr;
	unsigned int shadow_value;
} i2c_philips_registers;

/* Philips 7191 DMSD info. Shadow value is used as default */
static i2c_philips_registers DMSD_regs [] = {
	{ DMSD_REG_IDEL, 0x50 },
	{ DMSD_REG_HSYB, 0x30 },
	{ DMSD_REG_HSYS, 0x00 },
	{ DMSD_REG_HCLB, 0xe8 },
	{ DMSD_REG_HCLS, 0xb6 },
	{ DMSD_REG_HPHI, 0xf4 },
	{ DMSD_REG_LUMA, 0x01 },
	{ DMSD_REG_HUEC, 0x00 },
	{ DMSD_REG_CKTQ, 0xf8 },
	{ DMSD_REG_CKTS, 0xf8 },
	{ DMSD_REG_PLSE, 0x90 },
	{ DMSD_REG_SESE, 0x90 },
	{ DMSD_REG_GAIN, 0x00 },
	{ DMSD_REG_STDC, 0x8c },
	{ DMSD_REG_IOCK, 0x78 },
	{ DMSD_REG_CTL3, 0x99 },
	{ DMSD_REG_CTL4, 0x00 },
	{ DMSD_REG_CHCV, 0x2c },
	{ DMSD_REG_HS6B, 0x34 },
	{ DMSD_REG_HS6S, 0x0a },
	{ DMSD_REG_HC6B, 0xf4 },
	{ DMSD_REG_HC6S, 0xce },
	{ DMSD_REG_HP6I, 0xf4 },
	{ DMSD_REG_STATUS, -1 },
};

/* IndyCam info */
static i2c_indycam_registers IndyCam_regs [] = {
	{ INDYCAM_CONTROL,		0x01, I2C_REG_READ | I2C_REG_WRITE },
	{ INDYCAM_SHUTTER,		0x00, I2C_REG_READ | I2C_REG_WRITE },
	{ INDYCAM_GAIN,			0x80, I2C_REG_READ | I2C_REG_WRITE },
	{ INDYCAM_BRIGHTNESS,		  -1, I2C_REG_READ },
	{ INDYCAM_RED_BALANCE,		0x18, I2C_REG_READ | I2C_REG_WRITE },
	{ INDYCAM_BLUE_BALANCE,		0xa4, I2C_REG_READ | I2C_REG_WRITE },
	{ INDYCAM_RED_SATURATION,	0x80, I2C_REG_READ | I2C_REG_WRITE },
	{ INDYCAM_BLUE_SATURATION,	0xc0, I2C_REG_READ | I2C_REG_WRITE },
	{ INDYCAM_GAMMA,		  -1, I2C_REG_READ },
	{ INDYCAM_VERSION,		0x00, I2C_REG_READ },
	{ INDYCAM_RESET,		  -1, I2C_REG_WRITE },
	{ INDYCAM_LED,			  -1, I2C_REG_READ },
	{ INDYCAM_ORIENTATION		  -1, I2C_REG_READ },
	{ INDYCAM_BUTTON		  -1, I2C_REG_READ },
};

#define ELEMENTS(x)	(sizeof(x)/sizeof(x[0]))

/*---------------------------------------------------------------------------+
 |                            I2C bus access functions                       |
 +---------------------------------------------------------------------------*/

/* i2c bus timeout constants (black magic) */
#define I2C_ACK_TIMEOUT		1000
#define I2C_XFER_TIMEOUT	200
#define I2C_IDLE_TIMEOUT	200

/*
 * Waits for the vino asic to indicate that a data byte read transfer
 * from a slave device to the vino asic master chip has completed.
 * Return error on timeout.
 */
static int i2c_wait_xfer_done(void)
{
	int w = I2C_XFER_TIMEOUT;
	
	while (vino_reg_read(VINO_I2C_CTRL) & VINO_CTRL_I2C_TRANS_BUSY && w) {
		w--;
		udelay(1);
	}
	if (!w) {
		DEBUGI2C("i2c_wait_xfer_done: timeout.\n");
		return -ETIME;
	}
	return 0;
}

/*
 * Waits for the slave chip on the i2c bus to acknowledge transfer.
 * Return error on timeout.
 */
static int i2c_wait_ack(void)
{
	int err;
	int w = I2C_ACK_TIMEOUT;

	err = i2c_wait_xfer_done();
	if (err)
		return err;
	while (vino_reg_read(VINO_I2C_CTRL) & VINO_CTRL_I2C_ACK && w) {
		w--;
		udelay(1);
	}
	if (!w) {
		DEBUGI2C("i2c_wait_ack: timeout.\n");
		return -ETIME;
	}
	return 0;
}

/*
 * If the i2c bus is not idle then force it to do so. Report error on failure.
 */
static int i2c_force_idle(void)
{
	int w = I2C_IDLE_TIMEOUT;

	if ((vino_reg_read(VINO_I2C_CTRL) & VINO_CTRL_I2C_IDLE) == 0) {
		DEBUGI2C("i2c_force_idle: bus already idle.\n");
		return 0;
	}
	vino_reg_and(VINO_I2C_CTRL, ~(VINO_CTRL_I2C_IDLE));
	while (vino_reg_read(VINO_I2C_CTRL) & VINO_CTRL_I2C_IDLE && w) {
		w--;
		udelay(1);
	}
	if (!w) {
		DEBUGI2C("i2c_force_idle: timeout.\n");
		return -ETIME;
	}
	if (vino_reg_read(VINO_I2C_CTRL) & VINO_CTRL_I2C_BUS_ERROR) {
		DEBUGI2C("i2c_force_idle: bus error.\n");
		return -EIO;
	}
	return 0;
}

/*
 * Write out the i2c chip address in write mode and the reg subaddress.
 * If any i2c bus handshake errors are detected then report an error.
 */
static int i2c_write_subaddr(int i2c_chip, int reg_addr)
{
	if (i2c_force_idle())
		return -EIO;
	/* Write out the i2c chip address and specify a write operation */
	vino_reg_or(VINO_I2C_CTRL, VINO_CTRL_I2C_IDLE |
				   VINO_CTRL_I2C_MORE_BYTES);
	vino_reg_write(VINO_I2C_DATA, i2c_chip);
	if (i2c_wait_ack())
		return -EIO;
	/* write the register subaddress */
	vino_reg_write(VINO_I2C_DATA, reg_addr);
	if (i2c_wait_ack())
		return -EIO;
	return 0;
}

/*
 * Read the specified register in the specified i2c chip.
 * If any i2c bus handshake errors occur then report an error.
 */
static int i2c_read(int i2c_chip, int reg_addr)
{
	int i;
	unsigned long ret;

	DEBUGI2C("i2c_read: chipp_addr 0x%02x, reg_addr 0x%02x\n",
                 i2c_chip, reg_addr);
	/* The 7191 DMSD chip is write only except for the status register. 
	 * Use shadow values */
	if (i2c_chip == DMSD_ADDR && reg_addr != DMSD_REG_STATUS) {
		for (i = 0; i < ELEMENTS(DMSD_regs); i++) {
			if (DMSD_regs[i].reg_addr == reg_addr) {
				DEBUGI2C("i2c_read: read value 0x%04x\n",
                                         DMSD_regs[i].reg_addr);
				return DMSD_regs[i].shadow_value;
			}
		}
		return -EINVAL;
	}
	if (i2c_force_idle())
		return -EIO;
	/* Put the vino i2c master in the write state */
	vino_reg_or(VINO_I2C_CTRL, VINO_CTRL_I2C_MORE_BYTES |
				   VINO_CTRL_I2C_IDLE);
	/* For camera controller we have to set the reg subaddress before
	 * initiating the read. The 7191 status register does not require
	 * a subaddress to be set. */
	if (i2c_chip == INDYCAM_ADDR) {
		/* Write out the i2c chip address and specify a write
		 * operation */
		vino_reg_write(VINO_I2C_DATA, INDYCAM_ADDR);
		if (i2c_wait_ack())
			return -EIO;
		/* write the register subaddress */
		vino_reg_write(VINO_I2C_DATA, reg_addr);
		if (i2c_wait_ack())
			return -EIO;
		vino_reg_and(VINO_I2C_CTRL, ~VINO_CTRL_I2C_MORE_BYTES);
		if (i2c_force_idle())
			return -EIO;
		/* Put the vino i2c master in the write state */
		vino_reg_or(VINO_I2C_CTRL, VINO_CTRL_I2C_IDLE |
					   VINO_CTRL_I2C_MORE_BYTES);
	}
	/* Write out the i2c chip address and specify a read operation */
	vino_reg_write(VINO_I2C_DATA, i2c_chip | 1);
	if (i2c_wait_ack())
		return -EIO;
	/* Set read operation and wait for the data */
	vino_reg_or(VINO_I2C_CTRL, VINO_CTRL_I2C_IDLE | VINO_CTRL_I2C_DIR);
	if (i2c_wait_xfer_done())
		return -EIO;
	ret = vino_reg_read(VINO_I2C_DATA);
	vino_reg_and(VINO_I2C_CTRL, ~(VINO_CTRL_I2C_IDLE | VINO_CTRL_I2C_DIR |
				      VINO_CTRL_I2C_MORE_BYTES));
	DEBUGI2C("i2c_read: read value 0x%04lx\n", ret);
	return ret;
}

/*
 * Write out the specified value to the register in the specified i2c chip.
 * If any i2c bus handshake errors occur then report an error.
 */
static int i2c_write(int i2c_chip, int reg_addr, int val)
{
	int i;

	DEBUGI2C("i2c_write: chipp_addr 0x%02x, reg_addr 0x%02x, "
                 "val 0x%02x\n", i2c_chip, reg_addr, val);
	if (i2c_write_subaddr(i2c_chip, reg_addr) < 0)
		return -EIO;
	/* write the data value to the register in the i2c chip */
	vino_reg_write(VINO_I2C_DATA, val);
	if (i2c_wait_ack())
		return -EIO;
	if (i2c_chip == DMSD_ADDR && reg_addr != DMSD_REG_STATUS) {
		for (i = 0; i < ELEMENTS(DMSD_regs); i++)
			if (DMSD_regs[i].reg_addr == reg_addr)
				DMSD_regs[i].shadow_value = val;
	}
	return 0;
}

/*---------------------------------------------------------------------------+
 |                  Phillips chip and IndyCam access functions               |
 +---------------------------------------------------------------------------*/

/*
 * Write value to DMSD register
 */
static int write_dmsd_reg(int reg_addr, int mask, int shift, int val)
{
	int temp = i2c_read(DMSD_ADDR, reg_addr);

	if (temp < 0)
		return temp;
	temp &= mask;
	temp |= val << shift;
	return i2c_write(DMSD_ADDR, reg_addr, temp);
}

/*
 * Write default values to Philips registers
 */
static int write_phillips_defaults(void)
{
	int i;

	for (i = 0; i < ELEMENTS(DMSD_regs); i++) {
		if (DMSD_regs[i].shadow_value != -1) {
			if (i2c_write(DMSD_ADDR, DMSD_regs[i].reg_addr,
				      DMSD_regs[i].shadow_value)) {
				DEBUG("write_phillips_defaults: failed for "
				      "register 0x%02x.\n",
				      DMSD_regs[i].reg_addr);
				return -EIO;
			}
		}
	}
	return 0;
}

/*
 * Write default values to IndyCam registers
 */
static int write_camera_defaults(void)
{
	int i;

	for (i = 0; i < ELEMENTS(IndyCam_regs); i++) {
		if (IndyCam_regs[i].default_value != -1) {
			if (i2c_write(INDYCAM_ADDR, IndyCam_regs[i].reg_addr,
				      IndyCam_regs[i].default_value)) {
				DEBUG("write_camera_defaults: failed for "
				      "register 0x%02x.\n",
				      IndyCam_regs[i].reg_addr);
				return -EIO;
			}
		}
	}
	return 0;
}

/* 
 * Initialize Philips chip to default state
 */
static int init_phillips(void)
{
	int i = 3;
	int ret = -1;
	
	/* give Philips a chance ;-) */
	while (i > 0 && ret != 0) {
		ret = write_phillips_defaults();
		i--;
	}
	return ret;
}

/*
 * Initialize IndyCam to default state
 */
static int init_camera(void)
{
	int i = 3;
	int ret = -1;
	
	/* give IndyCam a chance ;-) */
	while (i > 0 && ret != 0) {
		ret = write_camera_defaults();
		i--;
	}
	return ret;
}

/* 
 * Check if IndyCam is attached to the system
 */
static int check_camera(void)
{
	int version;
	int retries = 3;

	camera_version = version = -1;
	while (retries > 0 && version == -1) {
		version = i2c_read(INDYCAM_ADDR, INDYCAM_VERSION);
		if (version == CAMERA_VERSION || 
		    version == MOOSE_CAMERA_VERSION) {
			camera_version = version;
			return 0;
		}
		else {
			version = -1;
			retries--;
		}
	}
	return -ENODEV;
}

/*
 * Set Phillips analog input and color mode. Report error on failure
 */
static int set_analog_input(int mux, int color_mode)
{
#define setdmsd(reg, mask, shift, val) \
	if (write_dmsd_reg(reg, mask, shift, val)) return -EIO;
	
	switch (mux) {
		/* set the chrominance input inactive, chrominance trap active, 
		 * set the GPSW1 and GPSW2 bits for Composite external input */
		case VINO_INPUT_COMPOSITE:
			setdmsd(DMSD_REG_IOCK, DMSD_GPSW_MASK, DMSD_GPSW_SHIFT,
				DMSD_GPSW_CVBS);
			switch (color_mode) {
				case VINO_COLORS_AUTO:
					setdmsd(DMSD_REG_IOCK, DMSD_CHRS_MASK,
						DMSD_CHRS_SHIFT, 0);
					setdmsd(DMSD_REG_GAIN, DMSD_COLO_MASK,
						DMSD_COLO_SHIFT, 0);
					setdmsd(DMSD_REG_LUMA, DMSD_BYPS_MASK,
						DMSD_BYPS_SHIFT, 0);
					break;
				case VINO_COLORS_MONO:
					setdmsd(DMSD_REG_IOCK, DMSD_CHRS_MASK,
						DMSD_CHRS_SHIFT, 0);
					setdmsd(DMSD_REG_GAIN, DMSD_COLO_MASK,
						DMSD_COLO_SHIFT, 0);
					setdmsd(DMSD_REG_LUMA, DMSD_BYPS_MASK,
						DMSD_BYPS_SHIFT, 1);
					break;
				case VINO_COLORS_COLOR:
					setdmsd(DMSD_REG_IOCK, DMSD_CHRS_MASK,
						DMSD_CHRS_SHIFT, 0);
					setdmsd(DMSD_REG_GAIN, DMSD_COLO_MASK,
						DMSD_COLO_SHIFT, 1);
					setdmsd(DMSD_REG_LUMA, DMSD_BYPS_MASK,
						DMSD_BYPS_SHIFT, 0);
					break;
			}
			break;
		/* set the chrominance input active, chrominance trap inactive,
		 * set the GPSW1 and GPSW2 bits for S-Video external input */
		case VINO_INPUT_SVIDEO:
			setdmsd(DMSD_REG_IOCK, DMSD_GPSW_MASK, DMSD_GPSW_SHIFT, 
				DMSD_GPSW_SVID);
			switch (color_mode) {
				case VINO_COLORS_AUTO:
					setdmsd(DMSD_REG_IOCK, DMSD_CHRS_MASK,
						DMSD_CHRS_SHIFT, 1);
					setdmsd(DMSD_REG_GAIN, DMSD_COLO_MASK,
						DMSD_COLO_SHIFT, 0);
					setdmsd(DMSD_REG_LUMA, DMSD_BYPS_MASK,
						DMSD_BYPS_SHIFT, 1);
					break;
				case VINO_COLORS_MONO:
					setdmsd(DMSD_REG_IOCK, DMSD_CHRS_MASK,
						DMSD_CHRS_SHIFT, 0);
					setdmsd(DMSD_REG_GAIN, DMSD_COLO_MASK,
						DMSD_COLO_SHIFT, 0);
					setdmsd(DMSD_REG_LUMA, DMSD_BYPS_MASK,
						DMSD_BYPS_SHIFT, 1);
					break;
				case VINO_COLORS_COLOR:
					setdmsd(DMSD_REG_IOCK, DMSD_CHRS_MASK,
						DMSD_CHRS_SHIFT, 1);
					setdmsd(DMSD_REG_GAIN, DMSD_COLO_MASK,
						DMSD_COLO_SHIFT, 1);
					setdmsd(DMSD_REG_LUMA, DMSD_BYPS_MASK,
						DMSD_BYPS_SHIFT, 1);
					break;
			}
			break;
		default:
			return -EINVAL;
	}
	return 0;
}

/*
 * Probe Phillips analog timing. If DMSD has anything connected then the Phase 
 * Lock Loop is locked and HLCK is set in the DMSD status register.
 * DMSD registers must be restored after call this function.
 */
static int probe_analog_timing(int busy_wait)
{
	int temp;
	int result = 0;

	analog_status = 0;
	temp = set_analog_input(VINO_INPUT_COMPOSITE, VINO_COLORS_AUTO);
	if (temp)
		return temp;
	if (busy_wait)
		mdelay(100);
	else {
		current->state = TASK_UNINTERRUPTIBLE;
		schedule_timeout(HZ / 10);
	}
	temp = i2c_read(DMSD_ADDR, DMSD_REG_STATUS);
	if (temp < 0)
		return temp;
	if ((temp & DMSD_STATUS_HLCK) == 0) {
		result |= VINO_COMPOSITE_ACTIVE;
		if (temp & DMSD_STATUS_FIDT)
			result |= VINO_COMPOSITE_NTSC;
	}
	
	temp = set_analog_input(VINO_INPUT_SVIDEO, VINO_COLORS_AUTO);
	if (temp)
		return temp;
	if (busy_wait)
		mdelay(100);
	else {
		current->state = TASK_UNINTERRUPTIBLE;
		schedule_timeout(HZ / 10);
	}
	temp = i2c_read(DMSD_ADDR, DMSD_REG_STATUS);
	if (temp < 0)
		return temp;
	if ((temp & DMSD_STATUS_HLCK) == 0) {
		result |= VINO_SVIDEO_ACTIVE;
		if (temp & DMSD_STATUS_FIDT)
			result |= VINO_SVIDEO_NTSC;
	}
	DEBUG("probe_analog_timing: %04x\n", result);
	analog_status = result;
	
	return 0;
}

/*---------------------------------------------------------------------------+
 |                                v4l helpers                                |
 +---------------------------------------------------------------------------*/

#ifdef CONFIG_PROC_FS
/*
 * Show some status informations
 */
static int vino_status_read_proc(char *buf, char **start, off_t off,
				 int count, int *eof, void *data)
{
		char *p = buf;

		p += sprintf(p, "Board revision:  0x%02x\n", board_rev);
		p += sprintf(p, "IndyCam version: ");
		if (camera_version == -1)
			p += sprintf(p, "not connected\n");
		else
			p += sprintf(p, "%d.%d\n", 
				     INDYCAM_VERSION_MAJOR(camera_version),
				     INDYCAM_VERSION_MINOR(camera_version));
		p += sprintf(p, "Composite:       %s\n", 
				(analog_status & VINO_COMPOSITE_ACTIVE) ?
				((analog_status & VINO_COMPOSITE_NTSC) ? 
				"NTSC" : "PAL" ) : "none");
		p += sprintf(p, "S-Video:         %s\n",
				(analog_status & VINO_SVIDEO_ACTIVE) ?
				((analog_status & VINO_SVIDEO_NTSC) ?
				"NTSC" : "PAL" ) : "none");
		p += sprintf(p, "Threshold:       %d, %d\n", 
				threshold_a, threshold_b);
		return p - buf;
}
#endif	/* CONFIG_PROC_FS */

static int bytes_per_pixel(struct vino_device *v)
{
	if (v->palette == VIDEO_PALETTE_GREY)
		return 1;
	if (v->palette == VIDEO_PALETTE_YUV422)
		return 2;
	/* VIDEO_PALETTE_RGB32 */
	return 4;
}

/*
 * Get capture size in pixels. In case of inactive analog input return NTSC
 * video size.
 */
static int get_capture_size(struct vino_device *v, int height)
{
	switch (v->input) {
		case VINO_INPUT_COMPOSITE:
			if ((analog_status & VINO_COMPOSITE_ACTIVE) &&
			   !(analog_status & VINO_COMPOSITE_NTSC))
				return (height) ? 
					VINO_PAL_HEIGHT : VINO_PAL_WIDTH;
			break;
		case VINO_INPUT_SVIDEO:
			if ((analog_status & VINO_SVIDEO_ACTIVE) &&
			   !(analog_status & VINO_SVIDEO_NTSC))
				return (height) ? 
					VINO_PAL_HEIGHT : VINO_PAL_WIDTH;
			break;
	}
	return (height) ? VINO_NTSC_HEIGHT : VINO_NTSC_WIDTH;
}

#define get_capture_height(v)	get_capture_size(v, 1)
#define get_capture_width(v)	get_capture_size(v, 0)

/*
 *  Get capture norm. In case of inactive analog input return NTSC.
 */
static int get_capture_norm(struct vino_device *v)
{
	switch (v->input) {
		case VINO_INPUT_COMPOSITE:
			if ((analog_status & VINO_COMPOSITE_ACTIVE) &&
			   !(analog_status & VINO_COMPOSITE_NTSC))
				return VIDEO_MODE_PAL;
			break;
		case VINO_INPUT_SVIDEO:
			if ((analog_status & VINO_SVIDEO_ACTIVE) &&
			   !(analog_status & VINO_SVIDEO_NTSC))
				return VIDEO_MODE_PAL;
			break;
	}
	return VIDEO_MODE_NTSC;
}

/*
 * Set clipping. Try new values to fit, if they don't return -EINVAL
 */
static int set_clipping(struct vino_device *v, int x, int y,
			int width, int height, int decimation)
{
	int maxwidth = get_capture_width(v);
	int maxheight = get_capture_height(v);
	
	DEBUG("set_clipping: x %d, y %d, width %d, height %d, decimation %d, "
	      "max width %d, max height %d\n", x, y, width, height, 
	      decimation, maxwidth, maxheight);
	if (x < 0 || y < 0 || width < 0 || height < 0 ||
	    x + VINO_MIN_WIDTH > maxwidth || width > maxwidth ||
	    y + VINO_MIN_HEIGHT > maxheight || height > maxheight)
		return -EINVAL;
	if (decimation < 1)
		decimation = 1;
	if (decimation > 8)
		decimation = 8;
	if (x + width > maxwidth)
		width = maxwidth - x;
	if (y + height > maxheight)
		height = maxheight - y;
	width = (bytes_per_pixel(v) * width) >> 3;
	width = (width << 3) / bytes_per_pixel(v);
	if (width < VINO_MIN_WIDTH || height < VINO_MIN_HEIGHT)
		return -EINVAL;
        v->left = x;
        v->top = y;
        v->right = x + width;
        v->bottom = y + height;
	v->decimation = decimation;
	v->line_size = bytes_per_pixel(v) * width;
	DEBUG("set_clipping: left %d, top %d, right %d, bottom %d, "
	      "decimation %d, line size %d\n", v->left, v->top, 
	      v->right, v->bottom, v->decimation, v->line_size);
	return 0;
}

/*
 * Load dummy page to descriptor registers. This prevents generating of
 * spurious interrupts
 */
static void load_dummy_buf(struct vino_device *v)
{
	vino_reg_write(v->base + VINO_PGIX, 0);
	vino_reg_write(v->base + VINO_LNCNT, 0);
	vino_reg_write(v->base + VINO_DESC_TLB_PTR, PHYSADDR(v->dummy_buf));
	vino_reg_write(v->base + VINO_DESC_PTR, PHYSADDR(v->dummy_buf));
}

static int alloc_dummy_buf(struct vino_device *v)
{
	int i;
	
	v->dummy_buf = get_zeroed_page(GFP_KERNEL);
	if (!v->dummy_buf)
		return -ENOMEM;
	for (i = 0; i < 10; i++)
		v->dummy_buf[i] = 0;
	return 0;
}

static void vino_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
	unsigned long intstat;
	int i;
	
	intstat = vino_reg_read(VINO_INTSTAT);
	if (intstat & VINO_INTSTAT_A_FIELD_TRANS) {
		vino_reg_and(VINO_CTRL, ~VINO_CTRL_A_DMA_ENBL); 
		vino_reg_and(VINO_INTSTAT, ~VINO_INTSTAT_A_FIELD_TRANS);
		Vino[VINO_CHAN_A].frame_ready = 1;
		DEBUG("irq arived. - we are done\n");
/*		wake_up_interruptible(&Vino[0].wait); */
	}
	if (intstat & VINO_INTSTAT_B_FIELD_TRANS) {
		vino_reg_and(VINO_CTRL, ~VINO_CTRL_B_DMA_ENBL); 
		vino_reg_and(VINO_INTSTAT, ~VINO_INTSTAT_B_FIELD_TRANS);
		Vino[VINO_CHAN_B].frame_ready = 1;
/*		wake_up_interruptible(&Vino[1].wait); */
	}
	if (intstat & VINO_INTSTAT_A_FIFO_OF) {
		vino_reg_and(VINO_CTRL, ~VINO_CTRL_A_DMA_ENBL);
		vino_reg_and(VINO_INTSTAT, ~VINO_INTSTAT_A_FIFO_OF);
		printk(KERN_ERR "VINO: FIFO overflow (channel A)!\n");
	}
	if (intstat & VINO_INTSTAT_B_FIFO_OF) {
		vino_reg_and(VINO_CTRL, ~VINO_CTRL_B_DMA_ENBL);
		vino_reg_and(VINO_INTSTAT, ~VINO_INTSTAT_B_FIFO_OF);
		printk(KERN_ERR "VINO: FIFO overflow (channel B)!\n");
	}
	/* should not happen... */
	if (intstat & VINO_INTSTAT_A_END_DESC_TBL) {
		vino_reg_and(VINO_CTRL, ~VINO_CTRL_A_DMA_ENBL);
		load_dummy_buf(&Vino[VINO_CHAN_A]);
		udelay(5);
		/* this locked my machine :( i can't see any solution */
                for (i = 10; 
		     i && (vino_reg_read(VINO_INTSTAT) & VINO_INTSTAT_A_END_DESC_TBL);
		     i--) {
			udelay(5);
                }
                if (i) {
			/* Something's gone wrong and EOD won't clear.
			 * Let's force it off, and punt. */
			vino_reg_and(VINO_INTSTAT, ~VINO_INTSTAT_A_END_DESC_TBL);
		}
		printk(KERN_ERR "VINO: end of descriptor table reached "
				"(channel A)!\n");
	}
	/* as above... */
	if (intstat & VINO_INTSTAT_B_END_DESC_TBL) {
		vino_reg_and(VINO_CTRL, ~VINO_CTRL_B_DMA_ENBL);
		load_dummy_buf(&Vino[VINO_CHAN_B]);
		vino_reg_and(VINO_INTSTAT, ~VINO_INTSTAT_B_END_DESC_TBL);
		printk(KERN_ERR "VINO: end of descriptor table reached "
				"(channel B)!\n");
	}
	vino_reg_write(VINO_INTSTAT, 0);
}

static int vino_request_irq(int irq)
{
	unsigned long flags;

	save_and_cli(flags);
	if (request_irq(irq, vino_interrupt, 0, vinostr, NULL)) {
		printk(KERN_ERR "VINO: Can't get irq %d\n", irq);
		restore_flags(flags);
		return -EAGAIN;
	}
	restore_flags(flags);
	return 0;
}

/* 
 * Prepare vino for DMA transfer... 
 */
static int dma_setup(struct vino_device *v)
{
	int i;
	unsigned long temp;
	
	DEBUG("dma_setup\n");
	for (i = 0; i < v->page_count; i++) 
		dma_cache_wback_inv(v->pages[i], PAGE_SIZE);
	dma_cache_wback_inv((unsigned long) v->phys_pages, PAGE_SIZE); 

	vino_reg_write(v->base + VINO_PGIX, 0);
	vino_reg_write(v->base + VINO_LNCNT, 0);
	/* VINO line size register is set 8 bytes less than actual */
	vino_reg_write(v->base + VINO_LNSZ, v->line_size - 8); 
	/* set the alpha register */
	vino_reg_write(v->base + VINO_ALPHA, v->alpha);
	/* set clip registers */
	vino_reg_write(v->base + VINO_CLIPS,
		       VINO_CLIP_START(v->left) | 
		       VINO_CLIP_ODD(v->top) | 
		       VINO_CLIP_EVEN(v->top + 1));
	vino_reg_write(v->base + VINO_CLIPE,
		       VINO_CLIP_START(v->right) | 
		       VINO_CLIP_ODD(v->bottom) | 
		       VINO_CLIP_EVEN(v->bottom + 1));
	/* init the frame rate and norm (full frame rate only for now...) */
	temp = VINO_FRAMERT_RT(0xFFF);
	if (get_capture_norm(v) == VIDEO_MODE_PAL)
		temp |= VINO_FRAMERT_PAL;
	vino_reg_write(v->base + VINO_FRAMERT, temp);
	temp = vino_reg_read(VINO_CTRL);
	/* select input interface */
	if (v->input == VINO_INPUT_CAMERA)
		temp |= (v->chan == VINO_CHAN_A) ?
			VINO_CTRL_A_SELECT : VINO_CTRL_B_SELECT;
	else
		temp &= (v->chan == VINO_CHAN_A) ?
			~VINO_CTRL_A_SELECT : ~VINO_CTRL_B_SELECT;
	/* enable synchronization */
	temp |= (v->chan == VINO_CHAN_A) ? 
		VINO_CTRL_A_SYNC_ENBL : VINO_CTRL_B_SYNC_ENBL;
	/* enable frame assembly */
	temp |= (v->chan == VINO_CHAN_A) ?
		VINO_CTRL_A_INTERLEAVE_ENBL : VINO_CTRL_B_INTERLEAVE_ENBL;
	/* set palette */
	temp &= (v->chan == VINO_CHAN_A) ?
		~(VINO_CTRL_A_LUMA_ONLY | VINO_CTRL_A_RGB | VINO_CTRL_A_DITHER) :
		~(VINO_CTRL_B_LUMA_ONLY | VINO_CTRL_B_RGB | VINO_CTRL_B_DITHER);
	switch (v->palette) {
		case VIDEO_PALETTE_GREY:
			temp |= (v->chan == VINO_CHAN_A) ? 
				VINO_CTRL_A_LUMA_ONLY : VINO_CTRL_B_LUMA_ONLY;
			break;
		case VIDEO_PALETTE_RGB32:
			temp |= (v->chan == VINO_CHAN_A) ?
				VINO_CTRL_A_RGB : VINO_CTRL_B_RGB;
			break;
#if 0
		/* FIXME: this is NOT in v4l API :-( */
		case VIDEO_PALETTE_RGB322:
			temp |= (v->chan == VINO_CHAN_A) ?
				VINO_CTRL_A_RGB | VINO_CTRL_A_DITHER : 
				VINO_CTRL_B_RGB | VINO_CTRL_B_DITHER;
			break;
#endif
	}
	vino_reg_write(VINO_CTRL, temp);
	/* set threshold level */
	vino_reg_write(v->base + VINO_FIFO_THRESHOLD, 
		       v->chan == VINO_CHAN_A ? threshold_a : threshold_b);
	/* let VINO know where to transfer data */
	vino_reg_write(v->base + VINO_DESC_TLB_PTR, PHYSADDR(v->phys_pages));
	vino_reg_write(v->base + VINO_DESC_PTR, PHYSADDR(v->phys_pages));
	/* give vino time to fetch the first four descriptors,
	 * 5 usec should be more than enough time */
	udelay(5);
	/* All interrupt conditions for this channel was cleared so clear 
	 * the interrupt status register and enable the interrupts for 
	 * this channel */
	if (v->chan == VINO_CHAN_A) {
		vino_reg_and(VINO_INTSTAT, 
			     ~(VINO_INTSTAT_A_FIELD_TRANS |
			       VINO_INTSTAT_A_FIFO_OF |	
			       VINO_INTSTAT_A_END_DESC_TBL));
		vino_reg_or(VINO_CTRL,
			    VINO_CTRL_A_FIELD_TRANS_INT |
			    VINO_CTRL_A_FIFO_OF_INT |
			    VINO_CTRL_A_END_DESC_TBL_INT);
	}
	else {
		vino_reg_and(VINO_INTSTAT,
			     ~(VINO_INTSTAT_B_FIELD_TRANS |
			       VINO_INTSTAT_B_FIFO_OF |	
			       VINO_INTSTAT_B_END_DESC_TBL));
		vino_reg_or(VINO_CTRL,
			    VINO_CTRL_B_FIELD_TRANS_INT |
			    VINO_CTRL_B_FIFO_OF_INT |
			    VINO_CTRL_B_END_DESC_TBL_INT);
	}
	return 0;
}

static void dma_stop(struct vino_device *v)
{
	vino_reg_and(VINO_CTRL, (v->chan == VINO_CHAN_A) ? 
		     ~VINO_CTRL_A_DMA_ENBL : ~VINO_CTRL_B_DMA_ENBL);
}

static void dma_go(struct vino_device *v)
{
	DEBUG("dma_go\n");
	vino_reg_or(VINO_CTRL, (v->chan == VINO_CHAN_A) ?
		    VINO_CTRL_A_DMA_ENBL : VINO_CTRL_B_DMA_ENBL);
}

static int vino_grab(struct vino_device *v, struct video_mmap *mem)
{
	return 0;
}

static int alloc_buffer(struct vino_device *v, int size)
{
	int i, npage;

	if (v->pages)
		return 0;
		
	npage = (size / PAGE_SIZE + 1);
	if (!(v->pages = (unsigned long*) kmalloc(npage * 
			  sizeof(unsigned long), GFP_KERNEL)))
		return -ENOMEM;
	v->phys_pages = (unsigned long *) get_zeroed_page(GFP_KERNEL);
	if (!v->phys_pages) {
		kfree(v->pages);
		v->pages = NULL;
		return -ENOMEM;
	}
	for (i = 0; i < npage; i++) {
		v->pages[i] = get_zeroed_page(GFP_KERNEL);
		if (!v->pages[i])
			break;
		v->phys_pages[i] = PHYSADDR((void *)v->pages[i]);
		mem_map_reserve(virt_to_page(v->pages[i]));
	}
	v->phys_pages[npage] = VINO_DESC_STOP;
	if (i-- < npage) {
		printk(KERN_DEBUG "VINO: grab buffer not allocated\n");
		for (; i >= 0; i--) {
			mem_map_unreserve(virt_to_page(v->pages[i]));
			free_page(v->pages[i]);
		}
		kfree(v->pages);
		free_page((unsigned long)v->phys_pages);
		v->pages = NULL;
		return -ENOBUFS;
	}
	v->page_count = npage;
	return 0;
}

static void free_buffer(struct vino_device *v)
{
	int i;

	for (i = 0; i < v->page_count; i++) {
		mem_map_unreserve(virt_to_page(v->pages[i]));
		free_page(v->pages[i]);
	}
	kfree(v->pages);
	free_page((unsigned long)v->phys_pages);
	v->pages = NULL;
	v->page_count = 0;       
}

/*---------------------------------------------------------------------------+
 |                             v4l functions                                 |
 +---------------------------------------------------------------------------*/

static int vino_open(struct video_device *dev, int flags)
{
	struct vino_device *vino = (struct vino_device *)dev;
	
	DEBUG("vino_open\n");
	if (vino->users)
		return -EBUSY;
	if (alloc_buffer(vino, VINO_FBUFSIZE))
		return -ENOBUFS;
	vino->status = 0;
	vino->users++;
	set_clipping(vino, 0, 0, get_capture_width(vino), get_capture_height(vino), 1);
	return 0;
}

static void vino_close(struct video_device *dev)
{
	struct vino_device *vino = (struct vino_device *)dev;
	
	dma_stop(vino);
	if (vino->pages)
		free_buffer(vino);
	vino->users--;
}

static int vino_mmap(struct video_device *dev, const char *adr,
		     unsigned long size)
{
	struct vino_device *vino = (struct vino_device *)dev;
	unsigned long start = (unsigned long)adr;
	int i;

	DEBUG("vino_mmap to address %p, size %ld\n", adr, size);
	/* sanity checks */
	if (!vino->pages)
		return -ENOBUFS;
	if (size > vino->page_count * PAGE_SIZE)
		return -EINVAL;
	
	for (i = 0; i < vino->page_count; i++) {
		unsigned long page = virt_to_phys((void *)vino->pages[i]);
		if (remap_page_range(start, page, PAGE_SIZE, PAGE_SHARED))
			return -EAGAIN;
		start += PAGE_SIZE;
		if (size <= PAGE_SIZE) break;
		size -= PAGE_SIZE;
	}
	return 0;
}

static int do_vino_ioctl(struct vino_device *vino, unsigned int cmd, void *arg)
{
	switch (cmd) {
	case VIDIOCGCAP: {
		struct video_capability v;

		strcpy(v.name, vinostr);
		v.type = VID_TYPE_CAPTURE | VID_TYPE_SUBCAPTURE;
		v.channels = (camera_version == -1) ? 2 : 3;
		v.audios = 0;
		v.maxwidth = 768;
		v.maxheight = 576;
		v.minwidth = 32;
		v.minheight = 32;
		if (copy_to_user(arg, &v, sizeof(v)))
			return -EFAULT;
		break;
	}
	case VIDIOCGCHAN: {
		struct video_channel v;

		if (copy_from_user(&v, arg, sizeof(v)))
			return -EFAULT;
		DEBUG("VIDIOCGCHAN channel %d\n", v.channel);
		v.flags = 0;
		v.tuners = 0;
		switch (v.channel) {
			case VINO_INPUT_COMPOSITE:
				v.norm = VIDEO_MODE_AUTO;
				v.type = VIDEO_TYPE_TV;
				strcpy(v.name, "Composite");
				break;
			case VINO_INPUT_SVIDEO:
				v.norm = VIDEO_MODE_AUTO;
				v.type = VIDEO_TYPE_TV;
				strcpy(v.name, "S-Video");
				break;
			case VINO_INPUT_CAMERA:
				v.norm = VIDEO_MODE_NTSC;
				v.type = VIDEO_TYPE_CAMERA;
				strcpy(v.name, "IndyCam");
				if (camera_version != -1)
					break;
			default:
				return -EINVAL;
		}
		if (copy_to_user(arg, &v, sizeof(v)))
			return -EFAULT;
		break;
	}
	case VIDIOCSCHAN: {
		struct video_channel v;
		int channels = (camera_version == -1) ? 2 : 3;

		if (copy_from_user(&v, arg, sizeof(v)))
			return -EFAULT;
		if (v.channel > channels - 1)
			return -EINVAL;
		vino->input = v.channel;
		break;
	}
	case VIDIOCGPICT: {
		struct video_picture v;

		/* TODO */
		v.brightness = 0x8000;
		v.hue = 0x8000;
		v.colour = 0x8000;
		v.contrast = 0x8000;
		v.whiteness = 0x8000;
		v.palette = vino->palette;
		if (v.palette == VIDEO_PALETTE_GREY)
			v.depth = 8;
		else if (v.palette == VIDEO_PALETTE_YUV422)
			v.depth = 16;
		else
			v.depth = 24;
		if (copy_to_user(arg, &v, sizeof(v)))
			return -EFAULT;
		break;
	}
	case VIDIOCSPICT: {
		struct video_picture v;

		if (copy_from_user(&v, arg, sizeof(v)))
			return -EFAULT;
		if (v.palette != VIDEO_PALETTE_GREY &&
		    v.palette != VIDEO_PALETTE_RGB32 &&
		    v.palette != VIDEO_PALETTE_YUV422)
			return -EINVAL;
		vino->palette = v.palette;
		break;
	}
	case VIDIOCGCAPTURE: {
		struct video_capture v;

		/* TODO: This would be nice once implemented... */
		v.x = vino->left;
		v.y = vino->top;
		v.width = vino->right - vino->left;
		v.height = vino->bottom - vino->top;
		v.decimation = vino->decimation;
		v.flags = 0;
		if (copy_to_user(arg, &v, sizeof(v)))
			return -EFAULT;
		break;
	}
	case VIDIOCSCAPTURE: {
		struct video_capture v;
		
		/* TODO: This would be nice once implemented... */
		if (copy_from_user(&v, arg, sizeof(v)))
			return -EFAULT;
		return set_clipping(vino, v.x, v.y, v.width, 
				    v.height, v.decimation);
	}
	case VIDIOCGMBUF: {
		struct video_mbuf v;
		int i, fo;

		v.size = vino->page_count * PAGE_SIZE;
		v.frames = vino->frames;
		/* FIXME */
		fo = 100000;
		for (i = 0; i < VIDEO_MAX_FRAME; i++)
			v.offsets[i] = i * fo;
		if (copy_to_user(arg, &v, sizeof(v)))
			return -EFAULT;
		break;
	}
	case VIDIOCMCAPTURE: {
		struct video_mmap v;
		int w = 50000;

		if (copy_from_user(&v, arg, sizeof(v)))
			return -EFAULT;
		DEBUG("VIDIOCMCAPTURE: frame %d, %dx%d\n",
		      v.frame, v.width, v.height);
		if (v.width != vino->right - vino->left ||
		    v.height != vino->bottom - vino->top ||
		    v.frame != 0)
			return -EINVAL;
		/* TODO: continuous capture */
		/* return vino_grab(vino, &v); */
		if (vino->input != VINO_INPUT_CAMERA)
			set_analog_input(vino->input, VINO_COLORS_COLOR);
		dma_setup(vino);
		udelay(100);
		dma_go(vino);
		/* this is _really_ ungly... */
		vino->frame_ready = 0;
		while (!vino->frame_ready && w) {
			w--;
			udelay(1);
		}
		dma_stop(vino);
		if (!vino->frame_ready) {
			DEBUG("timeout.\n");
//			return -ETIME;
		}
		break;
	}
	case VIDIOCSYNC: {
		int frame;

		/* FIXME: do what should be done here. see above */
		if (copy_from_user(&frame, arg, sizeof(frame)))
			return -EFAULT;
		break;
	}
	default:
		return -ENOIOCTLCMD;
	}
	return 0;
}

static int vino_ioctl(struct video_device *dev, unsigned int cmd, void *arg)
{
	struct vino_device *vino = (struct vino_device *)dev;
	int err;

	down(&vino->lock);
	err = do_vino_ioctl(vino, cmd, arg);
	up(&vino->lock);
	return err;
}

static struct video_device vino_template = {
	owner:		THIS_MODULE,
	type:		VID_TYPE_CAPTURE | VID_TYPE_SUBCAPTURE,
	hardware:	VID_HARDWARE_VINO,
	open:		vino_open,
	close:		vino_close,
	ioctl:		vino_ioctl,
	mmap:		vino_mmap,
};

static int init_device(void)
{
	unsigned short id;
	unsigned long foo;

#if 0
	/* not SGI Indy? */
	if (!sgi_guiness)
		return -ENODEV;
#endif

	foo = vino_reg_read(VINO_REVID);
	id = VINO_ID_VALUE(foo);
	board_rev = VINO_REV_NUM(foo);
	if (id != VINO_CHIP_ID) {
		printk(KERN_ERR "VINO not found (Rev/ID: 0x%04lx)\n", foo);
		return -ENODEV;
	}
	printk(KERN_INFO "VINO Rev: 0x%02hX\n", board_rev);
	
	vino_reg_write(VINO_CTRL, 0);
	/* prevents VINO from throwing spurious interrupts */
	load_dummy_buf(&Vino[VINO_CHAN_A]);
	load_dummy_buf(&Vino[VINO_CHAN_B]);
	udelay(5);
	vino_reg_write(VINO_INTSTAT, 0);

	if (init_phillips())
		printk(KERN_ERR "VINO: Phillips 7191 initialization failed.\n");
	if (check_camera())
		printk(KERN_INFO "VINO: IndyCam not connected.\n");
	else {
		printk(KERN_INFO "VINO: IndyCam v%d.%d detected.\n",
				 INDYCAM_VERSION_MAJOR(camera_version),
				 INDYCAM_VERSION_MINOR(camera_version));
		if (init_camera())
			printk(KERN_ERR "VINO: IndyCam initialization failed!\n");
	}
	probe_analog_timing(0);
	return vino_request_irq(SGI_VINO_IRQ);
}


static void init_struct(struct vino_device *v, int channel)
{
        init_MUTEX(&(v->lock));
	init_waitqueue_head(&v->wait);
        v->vdev = vino_template;
	strcpy(v->vdev.name, vinostr);
        v->chan = channel;
	v->input = VINO_INPUT_COMPOSITE;
	v->palette = VIDEO_PALETTE_GREY;
        v->users = 0;
        v->pages = 0;
	set_clipping(v, 0, 0, get_capture_width(v), get_capture_height(v), 1);
	/* XXX */
	v->frames = 1;
}

static int __init vino_init(void)
{
	int err;

	/* this have to be done here, it is used for descriptor
	 * registers initialization */
	Vino[VINO_CHAN_A].base = VINO_A_BASE;
	Vino[VINO_CHAN_B].base = VINO_B_BASE;
	err = alloc_dummy_buf(&Vino[VINO_CHAN_A]);
	if (err)
		return err;
	err = alloc_dummy_buf(&Vino[VINO_CHAN_B]);
	if (err) {
		free_page(Vino[VINO_CHAN_A].dummy_buf);
		return err;
	}
	
	err = init_device();
	if (err)
		goto error;

        init_struct(&Vino[VINO_CHAN_A], VINO_CHAN_A);
	if (video_register_device(&Vino[VINO_CHAN_A].vdev,
				  VFL_TYPE_GRABBER, video_nr) < 0) {
		printk("%s, chnl %d: device registration failed.\n",
			Vino[VINO_CHAN_A].vdev.name, Vino[VINO_CHAN_A].chan);
		err = -EINVAL;
		goto error;
	}
#if 0
	/* TODO: allow it once you write more code for video input locking */
        init_struct(&Vino[VINO_CHAN_B], VINO_CHAN_B);
	if (video_register_device(&Vino[VINO_CHAN_B].vdev, 
				  VFL_TYPE_GRABBER, video_nr) < 0) {
		printk("%s, chnl %d: device registration failed.\n",
			Vino[VINO_CHAN_B].vdev.name, Vino[VINO_CHAN_B].chan);
		video_unregister_device(&Vino[VINO_CHAN_A].vdev);
		err = -EINVAL;
		goto error;
	}
#endif
#ifdef CONFIG_PROC_FS
	create_proc_read_entry("driver/vino", 0, NULL, vino_status_read_proc, NULL);
#endif
	return 0;
error:
	free_irq(SGI_VINO_IRQ, NULL);
	free_page(Vino[VINO_CHAN_A].dummy_buf);
	free_page(Vino[VINO_CHAN_B].dummy_buf);
	return err;
}

static void __exit vino_exit(void)
{
	video_unregister_device(&Vino[VINO_CHAN_A].vdev);
#if 0
	video_unregister_device(&Vino[VINO_CHAN_B].vdev);
#endif
#ifdef CONFIG_PROC_FS
	remove_proc_entry("driver/vino", NULL);
#endif
	free_irq(SGI_VINO_IRQ, NULL);
	free_page(Vino[VINO_CHAN_A].dummy_buf);
	free_page(Vino[VINO_CHAN_B].dummy_buf);
}

module_init(vino_init);
module_exit(vino_exit);

MODULE_AUTHOR("Ladislav Michl");
MODULE_DESCRIPTION("Video4Linux driver for SGI Indy VINO (IndyCam)");
MODULE_LICENSE("GPL");
