// Simple utility to switch a Savage board between CRT/LCD devices.
// T. N. Roberts, 99-Aug-26.

// Linux x86 only.
// Modified by mADAR
// added S3 Virge/GX2 support


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#define extern
#include <sys/io.h>
#undef extern

#include "lrmi.h"

#define S3SWITCH_VERSION_STRING "0.1"

// Usage:
//  s3switch [-q] [crt|lcd|both]

// Define the Savage chip classes.  PCI id's stolen from xf86PciInfo.h

#define PCI_CHIP_VIRGEGX2	0x8A10
#define PCI_CHIP_SAVAGE3D	0x8A20
#define PCI_CHIP_SAVAGE3D_MV	0x8A21
#define PCI_CHIP_SAVAGE4	0x8A22
#define PCI_CHIP_SAVAGE2000	0x9102
#define PCI_CHIP_PROSAVAGE_PM	0x8A25
#define PCI_CHIP_PROSAVAGE_KM	0x8A26
#define PCI_CHIP_SAVAGE_MX_MV	0x8c10
#define PCI_CHIP_SAVAGE_MX	0x8c11
#define PCI_CHIP_SAVAGE_IX_MV	0x8c12
#define PCI_CHIP_SAVAGE_IX	0x8c13
#define PCI_CHIP_TWISTERP	0x8d01
#define PCI_CHIP_TWISTERK	0x8d02
#define PCI_CHIP_PROSAVAGE_DDR	0x8d03
#define PCI_CHIP_PROSAVAGE_DDRK	0x8d04
#define PCI_CHIP_SUPSAV_MX128	0x8c22
#define PCI_CHIP_SUPSAV_MX64	0x8c24
#define PCI_CHIP_SUPSAV_MX64C	0x8c26
#define PCI_CHIP_SUPSAV_IX128SDR	0x8c2a
#define PCI_CHIP_SUPSAV_IX128DDR	0x8c2b
#define PCI_CHIP_SUPSAV_IX64SDR	0x8c2c
#define PCI_CHIP_SUPSAV_IX64DDR	0x8c2d
#define PCI_CHIP_SUPSAV_IXCSDR	0x8c2e
#define PCI_CHIP_SUPSAV_IXCDDR	0x8c2f

enum {
  S3_VIRGEGX2,
  S3_SAVAGE3D,
  S3_SAVAGE4,
  S3_SAVAGEMXIX,
  S3_SAVAGE2000,
  S3_PROSAVAGE,
  S3_SUPERSAVAGE
} ChipClass;

// Define the device attachment bits.  This is CR6D on the non-mobile
// chips, and CR6B on the mobiles.

// Savage3D does not support LCD, and the Savage4 does not support TV.

#define CRT_ACTIVE      0x01
#define LCD_ACTIVE      0x02
#define TV_ACTIVE       0x04
#define CRT_ATTACHED    0x10
#define LCD_ATTACHED    0x20
#define TV_ATTACHED     0x40
#define DUO_ON		0x80

static char * devices[] = {
    " CRT", " LCD", " TV"
};

// Define the TV format bits in CR6B (non-mobile) or CRC0 (mobile).

#define TV_FORMAT_MASK	0x0c
#define TV_FORMAT_NTSCJ	0x00
#define TV_FORMAT_NTSC	0x04
#define TV_FORMAT_PAL	0x08

// Global state:

unsigned int gPCIid = 0;
unsigned char jTvFormat = 0;
unsigned char jDevices = 0;
unsigned char cr79 = 0;

void
usage()
{
    puts( "Usage: s3switch [-q] [crt|lcd|both|tv] [ntsc|ntscj|pal]" );
    puts( "  -q requests quiet operation." );
    puts( "  crt, lcd and tv activates output to those devices.  Several devices may be" );
    puts( "    specified.  Only devices which are actually attached may be activated." );
    puts( "  both is a shortcut for 'crt lcd'." );
    puts( "  ntscj, ntsc and pal specify the video format for TV output." );
    puts( "    This is supported on Savage3D only.");
    puts( "  With no parameters, displays all devices currently attached and active.");
}

void
IOAccess( int enable )
{
    /* Allow or disallow access to I/O ports. */

    ioperm( 0x40, 4, enable );
    ioperm( 0x4f, 1, enable );
    ioperm( 0x61, 1, enable );
    ioperm( 0x70, 1, enable );
    ioperm( 0x71, 1, enable );
    ioperm( 0x80, 1, enable );
    ioperm( 0xb2, 1, enable );
    ioperm( 0x3b0, 0x30, enable );
    ioperm( 0xeb, 1, enable );
}


void
fetch_bios_data()
{
    // Figure out what kind of Savage it is.

    outb( 0x2d, 0x3d4 );
    gPCIid = inb( 0x3d5 ) << 8;
    outb( 0x2e, 0x3d4 );
    gPCIid |= inb( 0x3d5 );

    switch( gPCIid ) {
	case PCI_CHIP_VIRGEGX2:
	    ChipClass = S3_VIRGEGX2;
	    break;
	case PCI_CHIP_SAVAGE3D:
	case PCI_CHIP_SAVAGE3D_MV:
	    ChipClass = S3_SAVAGE3D;
	    break;
	case PCI_CHIP_SAVAGE4:
	    ChipClass = S3_SAVAGE4;
	    break;
	case PCI_CHIP_SAVAGE2000:
	    ChipClass = S3_SAVAGE2000;
	    break;
	case PCI_CHIP_PROSAVAGE_PM:
	case PCI_CHIP_PROSAVAGE_KM:
	case PCI_CHIP_TWISTERP:
	case PCI_CHIP_TWISTERK:
	case PCI_CHIP_PROSAVAGE_DDR:
	case PCI_CHIP_PROSAVAGE_DDRK:
	    ChipClass = S3_PROSAVAGE;
	    break;
	case PCI_CHIP_SAVAGE_MX_MV:
	case PCI_CHIP_SAVAGE_MX:
	case PCI_CHIP_SAVAGE_IX_MV:
	case PCI_CHIP_SAVAGE_IX:
	    ChipClass = S3_SAVAGEMXIX;
	    break;
	case PCI_CHIP_SUPSAV_MX128:
	case PCI_CHIP_SUPSAV_MX64:
	case PCI_CHIP_SUPSAV_MX64C:
	case PCI_CHIP_SUPSAV_IX128SDR:
	case PCI_CHIP_SUPSAV_IX128DDR:
	case PCI_CHIP_SUPSAV_IX64SDR:
	case PCI_CHIP_SUPSAV_IX64DDR:
	case PCI_CHIP_SUPSAV_IXCSDR:
	case PCI_CHIP_SUPSAV_IXCDDR:
	    ChipClass = S3_SUPERSAVAGE;
	    break;
	default:
	    printf( "PCI id is not a recognized Savage or Virge GX2: %04x\n", gPCIid );
	    exit(-1);
    }

    if( ChipClass == S3_SAVAGEMXIX ) 
    {
	outb( 0xc0, 0x3d4 );
	jTvFormat = inb( 0x3d5 );
	outb( 0x6b, 0x3d4 );
	jDevices = inb( 0x3d5 );
    }
    else 
    {
	outb( 0x6b, 0x3d4 );
	jTvFormat = inb( 0x3d5 );
	outb( 0x6d, 0x3d4 );
	jDevices = inb( 0x3d5 );
    }

    outb( 0x79, 0x3d4 );
    cr79 = inb( 0x3d5 );

    //printf( "Device ID: %04x\n", gPCIid);

    // The Savage4 and Savage2000 are the only chips which actually detect
    // the presence of the devices.  For the others, we just have to assume.

    switch( ChipClass )
    {
	case S3_VIRGEGX2:
	    jDevices = (jDevices & 0x0f) 
		| CRT_ATTACHED 
		| TV_ATTACHED;
	    break;

	case S3_SAVAGE3D:
	    jDevices = (jDevices & 0x0f) 
		| CRT_ATTACHED 
		| TV_ATTACHED;
	    break;

	case S3_SAVAGE4:
	case S3_SAVAGE2000:
	    /* These two get it right. */
	    break;

	case S3_SAVAGEMXIX:
	case S3_PROSAVAGE:
	case S3_SUPERSAVAGE:
	default:
	    jDevices = (jDevices & 0x0f) 
		| CRT_ATTACHED 
		| TV_ATTACHED 
		| LCD_ATTACHED;
	    break;
    }
}


unsigned short
set_active_device( int iDevice )
{
    struct LRMI_regs r;
    int iResult = 0;

    if (!LRMI_init())
	return 1;

    /* Go set the active device. */

    memset( &r, 0, sizeof(r) );

    r.eax = 0x4f14;	// S3 extended functions
    r.ebx = 0x0003;	// set active device
    r.ecx = iDevice;

    if( ChipClass == S3_SAVAGEMXIX )
	r.ecx |= DUO_ON;

    iResult = LRMI_int( 0x10, &r );

    if( !iResult )
    {
	fprintf( stderr, "Could not set device (vm86 failure)\n" );
	return 1;
    }

    if ( (r.eax & 0xffff) != 0x4f )
    {
	fprintf( stderr, "BIOS returned error code.\n" );
	return 1;
    }

    return 0;
}



unsigned short
set_tv_state( int state )
{
    struct LRMI_regs r;
    int iResult = 0;

    if (!LRMI_init())
	return 1;

    /* And go set the TV state. */

    memset( &r, 0, sizeof(r) );

    r.eax = 0x4f14;	// S3 extended functions
    r.ebx = 0x0007;	// set tv state
    r.ecx = state;
    r.edx = TV_FORMAT_MASK;

    iResult = LRMI_int( 0x10, &r );

    if( !iResult )
    {
	fprintf( stderr, "Could not set TV state (vm86 failure)\n" );
	return 1;
    }

    if ( (r.eax & 0xffff) != 0x4f )
    {
	fprintf( stderr, "BIOS returned error code.\n" );
	return 1;
    }

    return 0;
}

void
print_current_state()
{
    int i;

    printf( "Devices attached: " );

    if( !(jDevices & 0x70) )
    {
        // How can this be?
        printf( "none" );
    }
    else
        for( i = 0; i < 3; i++ )
            if( jDevices & (0x10 << i) )
                printf( "%s", devices[i] );

    printf( "\nDevices active:   " );

    if( !(jDevices & 0x07) )
    {
        // How can this be?
        printf( "none\n" );
    }
    else
        for( i = 0; i < 3; i++ )
            if( jDevices & (0x01 << i) )
                printf( "%s", devices[i] );

    if( jDevices & TV_ATTACHED )
    {
        static char * szTV[] = { "NTSC-J", "NTSC", "PAL" };

	printf( 
	    "\nCurrent TV format is %s", 
	    szTV[(jTvFormat & TV_FORMAT_MASK) >> 2]
	);
    }

    printf( "\n" );
}


void
set_new_state( int newstate )
{
    // We should prohibit TV on Savage4.

    if( ((jDevices >> 4) & newstate) != newstate )
    {
        fprintf( stderr, "You attempted to activate a device which is not connected.\n" );
        // Alternatively, quiet = 0, return.
        print_current_state();
        exit( -2 );
    }

    set_active_device( newstate );

    // If the LCD state changed, we need to adjust cr79 in Savage4.
    // These values are somewhat magical, and are set by the X server.

    if( (ChipClass == S3_SAVAGE4) || (ChipClass == S3_SAVAGE2000) )
    {
	if( (jDevices & LCD_ACTIVE) && !(newstate & LCD_ACTIVE) )
	{
	    // The LCD was alive and now it isn't.  We can increase cr79.

	    if( (cr79 == 5) || (cr79 == 8) )
	    {
		cr79 = (cr79 == 5) ? 8 : 0x0e;
		ioperm( 0x3d4, 2, 1 );
		outw( (cr79 << 8) | 0x79, 0x3d4 );
		ioperm( 0x3d4, 2, 0 );
	    }
	}
	else if( !(jDevices & LCD_ACTIVE) && (newstate & LCD_ACTIVE) )
	{
	    // The LCD was off and now it's on.  We must cut back cr79.

	    if( (cr79 == 8) || (cr79 == 0xe) )
	    {
		cr79 = (cr79 == 8) ? 5 : 8;
		ioperm( 0x3d4, 2, 1 );
		outw( (cr79 << 8) | 0x79, 0x3d4 );
		ioperm( 0x3d4, 2, 0 );
	    }
	}
    }

    fetch_bios_data();

    return;
}


void
set_new_tvstate( int tvstate )
{
    if( ChipClass == S3_SAVAGE4 )
	return;

    set_tv_state( tvstate );

    fetch_bios_data();

    return;
}


int
main( int argc, char ** argv )
{
    int quiet = 0;
    int newstate = 0;
    int newtv = 0;

    // Scan through the argument list.  We do very primitive checking here.

    while( *++argv )
    {
	if( strcmp( *argv, "-q" ) == 0 )
	    quiet++;
	else if( strcasecmp( *argv, "crt" ) == 0 )
	    newstate |= CRT_ACTIVE;
	else if( strcasecmp( *argv, "lcd" ) == 0 )
	    newstate |= LCD_ACTIVE;
	else if( strcasecmp( *argv, "both" ) == 0 )
	    newstate |= CRT_ACTIVE | LCD_ACTIVE;
	else if( strcasecmp( *argv, "tv" ) == 0 )
	    newstate |= TV_ACTIVE;
	else if( strcasecmp( *argv, "ntsc-j" ) == 0 )
	    newtv = TV_FORMAT_NTSCJ;
	else if( strcasecmp( *argv, "ntscj" ) == 0 )
	    newtv = TV_FORMAT_NTSCJ;
	else if( strcasecmp( *argv, "ntsc" ) == 0 )
	    newtv = TV_FORMAT_NTSC;
	else if( strcasecmp( *argv, "pal" ) == 0 )
	    newtv = TV_FORMAT_PAL;
	else if( strcmp( *argv, "-V" ) == 0 )
	{
	    printf("s3switch " S3SWITCH_VERSION_STRING "\n");
	    exit( 0 );
	}
	else if( strcmp( *argv, "-h" ) == 0 )
	{
	    usage();
	    exit( 0 );
	}
	else
	{
	    fprintf( stderr, "Unknown argument: %s\n", *argv );
	    usage();
	    exit( -1 );
	}
    }
    
    if( geteuid() != 0 )
    {
	fprintf( stderr, "s3switch must be setuid root.\n" );
        exit( -1 );
    }

    IOAccess( 1 );

    fetch_bios_data();

    if( newtv )
        set_new_tvstate( newtv );

    if( newstate )
        set_new_state( newstate );

    if( !quiet )
        print_current_state( );

    IOAccess( 0 );

    return 0;
}
