/*
	File:		MacGamma.cpp

	Contains:	Functions to enable Mac OS device gamma adjustments using 3 channel 256 element 8 bit gamma ramps

	Written by:	Geoff Stahl (ggs)

	Copyright:	Copyright © 1999 Apple Computer, Inc., All Rights Reserved

	Change History (most recent first):

         <7>     8/25/99    ggs     Added usage notes
         <6>     8/25/99    ggs     Fixed handling of 1 -> 3 channel gamma conversions in get and set
         <5>     7/14/99    ggs     Added sample code notice
         <4>     5/20/99    ggs     Added handling for gamma tables with different data widths,
                                    number of entries, and channels.  Forced updates to 3 channels
                                    (poss. could break on rare card, but very unlikely).  Added
                                    quick update with BlockMove for 3x256x8 tables. Updated function
                                    names.
         <3>     5/20/99    ggs     Cleaned up and commented
         <2>     5/20/99    ggs     Added system wide get and restore gamma functions to enable
                                    restoration of original for all devices.  Modified functionality
                                    to return pointers vice squirreling away the memory.
         <1>     5/20/99    ggs     Initial Add

	Disclaimer:	You may incorporate this sample code into your applications without
				restriction, though the sample code has been provided "AS IS" and the
				responsibility for its operation is 100% yours.  However, what you are
				not permitted to do is to redistribute the source as "DSC Sample Code"
				after having made changes. If you're going to re-distribute the source,
				we require that you make it clear in the source that the code was
				descended from Apple Sample Code, but that you've made changes.

*/

// Usage notes: 

// SetDeviceGammaRampXX may not return until the VBL following their call, depending on the display driver,
// this is due to the handling of the Control call by the driver.

// Startup and Shutdown:

// GetSystemGammas (...) must be called to save the system gammas prior to using MacGamma
// RestoreSystemGamms (...)  must be called before exiting to restore original gamma
// DisposeSystemGammas (...) can then be used to dispose of the system gammas

// Suspend and Resume:

// RestoreSystemGamms (...); DisposeSystemGammas (...); should also be called prior to suspend and
// GetSystemGammas (...) should be called when resuming
// this prevents users from saving display settings with your gammas


// system includes ----------------------------------------------------------

#include <Devices.h>
#include <Files.h>
#include <MacTypes.h>
#include <QDOffscreen.h>
#include <Quickdraw.h>
#include <video.h>


// project includes ---------------------------------------------------------

#include "MacGamma.h"


// structures (internal/private) -----------------------------------------------
	
struct recDeviceGamma											// storage for device handle and gamma table
{
	GDHandle hGD;												// handle to device
	GammaTblPtr pDeviceGamma;									// pointer to device gamma table
};
typedef recDeviceGamma * precDeviceGamma;

struct recSystemGamma											// storage for system devices and gamma tables
{
	short numDevices;											// number of devices
	precDeviceGamma * devGamma;									// array of pointers to device gamma records
};
typedef recSystemGamma * precSystemGamma;

extern "C" {

// prototypes (internal/private) --------------------------------------------

// 5/20/99: (ggs) changed functional specification

OSErr GetGammaTable(GDHandle gd, GammaTblPtr * ppTableGammaOut);		// Returns the device gamma table pointer in ppDeviceTable
Ptr CreateEmptyGammaTable (short channels, short entries, short bits);	// creates an empty gamma table of a given size, assume no formula data will be used
Ptr CopyGammaTable (GammaTblPtr pTableGammaIn);							// given a pointer toa device gamma table properly iterates and copies
void DisposeGammaTable (Ptr pGamma);									// disposes gamma table returned from GetGammaTable, GetDeviceGamma, or CopyGammaTable 
Ptr GetDeviceGamma (GDHandle hGD);										// returns pointer to copy of orginal device gamma table in native format
void RestoreDeviceGamma (GDHandle hGD, Ptr pGammaTable);				// sets device to saved table

// functions (internal/private) ---------------------------------------------

// GetGammaTable

// Returns the device gamma table pointer in ppTableGammaOut

OSErr GetGammaTable (GDHandle hGD, GammaTblPtr * ppTableGammaOut)
{
	VDGammaRecord   DeviceGammaRec;
	CntrlParam		cParam;
	OSErr			err;
	
	cParam.ioCompletion = NULL;										// set up control params
	cParam.ioNamePtr = NULL;
	cParam.ioVRefNum = 0;
	cParam.ioCRefNum = (**hGD).gdRefNum;
	cParam.csCode = cscGetGamma;									// Get Gamma commnd to device
	*(Ptr *)cParam.csParam = (Ptr) &DeviceGammaRec;					// record for gamma

	err = PBStatus( (ParmBlkPtr)&cParam, 0 );						// get gamma
	
	*ppTableGammaOut = (GammaTblPtr)(DeviceGammaRec.csGTable);		// pull table out of record
	
	return err;	
}

// --------------------------------------------------------------------------

// CreateEmptyGammaTable

// creates an empty gamma table of a given size, assume no formula data will be used

Ptr CreateEmptyGammaTable (short channels, short entries, short bits)
{
	GammaTblPtr		pTableGammaOut = NULL;
	short			tableSize, dataWidth;

	dataWidth = (bits + 7) / 8;										// number of bytes per entry
	tableSize = sizeof (GammaTbl) + (channels * entries * dataWidth);
	pTableGammaOut = (GammaTblPtr) NewPtrClear (tableSize);			// allocate new tabel

	if (pTableGammaOut)												// if we successfully allocated
	{
		pTableGammaOut->gVersion = 0;								// set parameters based on input
		pTableGammaOut->gType = 0;
		pTableGammaOut->gFormulaSize = 0;
		pTableGammaOut->gChanCnt = channels;
		pTableGammaOut->gDataCnt = entries;
		pTableGammaOut->gDataWidth = bits;
	}
	return (Ptr)pTableGammaOut;										// return whatever we allocated
}

// --------------------------------------------------------------------------

// CopyGammaTable

// given a pointer toa device gamma table properly iterates and copies

Ptr CopyGammaTable (GammaTblPtr pTableGammaIn)
{
	GammaTblPtr		pTableGammaOut = NULL;
	short			tableSize, dataWidth;

	if (pTableGammaIn)												// if there is a table to copy 
	{
		dataWidth = (pTableGammaIn->gDataWidth + 7) / 8;			// number of bytes per entry
		tableSize = sizeof (GammaTbl) + pTableGammaIn->gFormulaSize +
					(pTableGammaIn->gChanCnt * pTableGammaIn->gDataCnt * dataWidth);
		pTableGammaOut = (GammaTblPtr) NewPtr (tableSize);			// allocate new table
		if (pTableGammaOut)											
			BlockMove( (Ptr)pTableGammaIn, (Ptr)pTableGammaOut, tableSize);	// move everything
	}
	return (Ptr)pTableGammaOut;										// return whatever we allocated, could be NULL
}

// --------------------------------------------------------------------------

// DisposeGammaTable

// disposes gamma table returned from GetGammaTable, GetDeviceGamma, or CopyGammaTable 
// 5/20/99: (ggs) added

void DisposeGammaTable (Ptr pGamma)
{
	if (pGamma)
		DisposePtr((Ptr) pGamma);									// get rid of it
}

// --------------------------------------------------------------------------

// GetDeviceGamma

// returns pointer to copy of orginal device gamma table in native format (allocates memory for gamma table, call DisposeDeviceGamma to delete)
// 5/20/99: (ggs) change spec to return the allocated pointer vice storing internally

Ptr GetDeviceGamma (GDHandle hGD)
{
	GammaTblPtr		pTableGammaDevice = NULL;
	GammaTblPtr		pTableGammaReturn = NULL;	
	OSErr			err;
	
	err = GetGammaTable (hGD, &pTableGammaDevice);					// get a pointer to the devices table
	if ((noErr == err) && pTableGammaDevice)						// if succesful
		pTableGammaReturn = (GammaTblPtr) CopyGammaTable (pTableGammaDevice); // copy to global

	return (Ptr) pTableGammaReturn;
}

// --------------------------------------------------------------------------

// RestoreDeviceGamma

// sets device to saved table
// 5/20/99: (ggs) now does not delete table, avoids confusion

void RestoreDeviceGamma (GDHandle hGD, Ptr pGammaTable)
{
	VDSetEntryRecord setEntriesRec;
	VDGammaRecord	gameRecRestore;
	CTabHandle      hCTabDeviceColors;
	Ptr				csPtr;
	OSErr			err = noErr;
	
	if (pGammaTable)												// if we have a table to restore								
	{
		gameRecRestore.csGTable = pGammaTable;						// setup restore record
		csPtr = (Ptr) &gameRecRestore;
		err = Control((**hGD).gdRefNum, cscSetGamma, (Ptr) &csPtr);	// restore gamma

		if ((noErr == err) && (8 == (**(**hGD).gdPMap).pixelSize))	// if successful and on an 8 bit device
		{
			hCTabDeviceColors = (**(**hGD).gdPMap).pmTable;			// do SetEntries to force CLUT update
			setEntriesRec.csTable = (ColorSpec *) &(**hCTabDeviceColors).ctTable;
			setEntriesRec.csStart = 0;
			setEntriesRec.csCount = (**hCTabDeviceColors).ctSize;
			csPtr = (Ptr) &setEntriesRec;
			
			err = Control((**hGD).gdRefNum, cscSetEntries, (Ptr) &csPtr); // SetEntries in CLUT
		}
	}
}


// functions (external/public) ----------------------------------------------

// GetSystemGammas

// returns a pointer to a set of all current device gammas in native format (returns NULL on failure, which means reseting gamma will not be possible)
// 5/20/99: (ggs) added

Ptr GetSystemGammas (void)										
{
	precSystemGamma pSysGammaOut;									// return pointer to system device gamma info
	short devCount = 0;												// number of devices attached
	Boolean fail = false;

	pSysGammaOut = (precSystemGamma) NewPtr (sizeof (recSystemGamma)); // allocate for structure
	
	GDHandle hGDevice = GetDeviceList ();							// top of device list
	do																// iterate
	{
		devCount++;													// count devices					
		hGDevice = GetNextDevice (hGDevice);						// next device
	} while (hGDevice);
	
	pSysGammaOut->devGamma = (precDeviceGamma *) NewPtr (sizeof (precDeviceGamma) * devCount); // allocate for array of pointers to device records
	if (pSysGammaOut)
	{
		pSysGammaOut->numDevices = devCount;						// stuff count
		
		devCount = 0;												// reset iteration
		hGDevice = GetDeviceList ();
		do
		{
			pSysGammaOut->devGamma [devCount] = (precDeviceGamma) NewPtr (sizeof (recDeviceGamma));	  // new device record
			if (pSysGammaOut->devGamma [devCount])					// if we actually allocated memory
			{
				pSysGammaOut->devGamma [devCount]->hGD = hGDevice;										  // stuff handle
				pSysGammaOut->devGamma [devCount]->pDeviceGamma = (GammaTblPtr)GetDeviceGamma (hGDevice); // copy gamma table
			}
			else													// otherwise dump record on exit
			 fail = true;
			devCount++;												// next device
			hGDevice = GetNextDevice (hGDevice);						
		} while (hGDevice);
	}
	if (!fail)														// if we did not fail
		return (Ptr) pSysGammaOut;									// return pointer to structure
	else
	{
		DisposeSystemGammas (&(Ptr)pSysGammaOut);					// otherwise dump the current structures (dispose does error checking)
		return NULL;												// could not complete
	}
}

// --------------------------------------------------------------------------

// RestoreSystemGammas

// restores all system devices to saved gamma setting
// 5/20/99: (ggs) added

void RestoreSystemGammas (Ptr pSystemGammas)
{
	precSystemGamma pSysGammaIn = (precSystemGamma) pSystemGammas;
	if (pSysGammaIn)
		for (short i = 0; i < pSysGammaIn->numDevices; i++)			// for all devices
			RestoreDeviceGamma (pSysGammaIn->devGamma [i]->hGD, (Ptr) pSysGammaIn->devGamma [i]->pDeviceGamma);	// restore gamma
}

// --------------------------------------------------------------------------

// DisposeSystemGammas

// iterates through and deletes stored gamma settings
// 5/20/99: (ggs) added

void DisposeSystemGammas (Ptr* ppSystemGammas)
{
	precSystemGamma pSysGammaIn;
	if (ppSystemGammas)
	{
		pSysGammaIn = (precSystemGamma) *ppSystemGammas;
		if (pSysGammaIn)
		{
			for (short i = 0; i < pSysGammaIn->numDevices; i++)		// for all devices
				if (pSysGammaIn->devGamma [i])						// if pointer is valid
				{
					DisposeGammaTable ((Ptr) pSysGammaIn->devGamma [i]->pDeviceGamma); // dump gamma table
					DisposePtr ((Ptr) pSysGammaIn->devGamma [i]);					   // dump device info
				}
			DisposePtr ((Ptr) pSysGammaIn->devGamma);				// dump device pointer array		
			DisposePtr ((Ptr) pSysGammaIn);							// dump system structure
			*ppSystemGammas = NULL;
		}	
	}
}

// --------------------------------------------------------------------------

// GetDeviceGammaRampGD

// retrieves the gamma ramp from a graphics device (pRamp: 3 arrays of 256 elements each)

Boolean GetDeviceGammaRampGD (GDHandle hGD, Ptr pRamp)
{
	GammaTblPtr		pTableGammaTemp = NULL;
	long 			indexChan, indexEntry;
	OSErr			err;
	
	if (pRamp)															// ensure pRamp is allocated
	{
		err = GetGammaTable (hGD, &pTableGammaTemp);					// get a pointer to the current gamma
		if ((noErr == err) && pTableGammaTemp)							// if successful
		{															
			// fill ramp
			unsigned char * pEntry = (unsigned char *) &pTableGammaTemp->gFormulaData + pTableGammaTemp->gFormulaSize; // base of table
			short bytesPerEntry = (pTableGammaTemp->gDataWidth + 7) / 8; // size, in bytes, of the device table entries
			short shiftRightValue = pTableGammaTemp->gDataWidth - 8; 	 // number of right shifts device -> ramp
			short channels = pTableGammaTemp->gChanCnt;	
			short entries = pTableGammaTemp->gDataCnt;									
			if (3 == channels)											// RGB format
			{															// note, this will create runs of entries if dest. is bigger (not linear interpolate)
				for (indexChan = 0; indexChan < channels; indexChan++)
					for (indexEntry = 0; indexEntry < 256; indexEntry++)
						*((unsigned char *) pRamp + (indexChan * 256) + indexEntry) = 
						  *(pEntry + indexChan * entries * bytesPerEntry + indexEntry * entries * bytesPerEntry / 256) >> shiftRightValue;
			}
			else														// single channel format
			{
				for (indexChan = 0; indexChan < 768; indexChan += 256)	// repeat for all 3 channels (step by ramp size)
					for (indexEntry = 0; indexEntry < 256; indexEntry++) // for all entries set vramp value
						*((unsigned char *) pRamp + indexChan + indexEntry) = 
						  *(pEntry + indexEntry * entries * bytesPerEntry / 256) >> shiftRightValue;
			}
			return true;
		}
	}
	return false;
}

// --------------------------------------------------------------------------

// GetDeviceGammaRampGW

// retrieves the gamma ramp from a graphics device associated with a GWorld pointer (pRamp: 3 arrays of 256 elements each)

Boolean GetDeviceGammaRampGW (GWorldPtr pGW, Ptr pRamp)
{
	GDHandle hGD = GetGWorldDevice (pGW);
	return GetDeviceGammaRampGD (hGD, pRamp);
}

// --------------------------------------------------------------------------

// GetDeviceGammaRampCGP

// retrieves the gamma ramp from a graphics device associated with a CGraf pointer (pRamp: 3 arrays of 256 elements each)

Boolean GetDeviceGammaRampCGP (CGrafPtr pGraf, Ptr pRamp)
{
	CGrafPtr pGrafSave;
	GDHandle hGDSave;
	GetGWorld (&pGrafSave, &hGDSave);
	SetGWorld (pGraf, NULL);
	GDHandle hGD = GetGDevice ();
	Boolean fResult = GetDeviceGammaRampGD (hGD, pRamp);
	SetGWorld (pGrafSave, hGDSave);
	return fResult;
}

// --------------------------------------------------------------------------

// SetDeviceGammaRampGD

// sets the gamma ramp for a graphics device (pRamp: 3 arrays of 256 elements each (R,G,B))

Boolean SetDeviceGammaRampGD (GDHandle hGD, Ptr pRamp)
{
	VDSetEntryRecord setEntriesRec;
	VDGammaRecord	gameRecRestore;
	GammaTblPtr		pTableGammaNew;
	GammaTblPtr		pTableGammaCurrent = NULL;
	CTabHandle      hCTabDeviceColors;
	Ptr				csPtr;
	OSErr			err;
	short 			dataBits, entries, channels = 3;						// force three channels in the gamma table
	
	if (pRamp)																// ensure pRamp is allocated
	{
		err= GetGammaTable (hGD, &pTableGammaCurrent);						// get pointer to current table
		if ((noErr == err) && pTableGammaCurrent)
		{
			dataBits = pTableGammaCurrent->gDataWidth;						// table must have same data width
			entries = pTableGammaCurrent->gDataCnt;							// table must be same size
			pTableGammaNew = (GammaTblPtr) CreateEmptyGammaTable (channels, entries, dataBits); // our new table
			if (pTableGammaNew)												// if successful fill table
			{	
				unsigned char * pGammaBase = (unsigned char *) &pTableGammaNew->gFormulaData + pTableGammaNew->gFormulaSize; // base of table
				if ((256 == entries) && (8 == dataBits)) 						// simple case: direct mapping
					BlockMove ((Ptr)pRamp, (Ptr)pGammaBase, channels * entries); // move everything
				else														// tough case handle entry, channel and data size disparities
				{
					short bytesPerEntry = (dataBits + 7) / 8; 				// size, in bytes, of the device table entries
					short shiftRightValue = 8 - dataBits;					// number of right shifts ramp -> device
					shiftRightValue += ((bytesPerEntry - 1) * 8);  			// multibyte entries and the need to map a byte at a time most sig. to least sig.
					for (short indexChan = 0; indexChan < channels; indexChan++) // for all the channels
						for (short indexEntry = 0; indexEntry < entries; indexEntry++) // for all the entries
						{
							short currentShift = shiftRightValue;			// reset current bit shift
							long temp = *((unsigned char *)pRamp + (indexChan << 8) + (indexEntry << 8) / entries); // get data from ramp
							for (short indexByte = 0; indexByte < bytesPerEntry; indexByte++) // for all bytes
							{
								if (currentShift < 0)						// shift data correctly for current byte
									*(pGammaBase++) = temp << -currentShift;
								else
									*(pGammaBase++) = temp >> currentShift;
								currentShift -= 8;							// increment shift to align to next less sig. byte
							}
						}
				}
				
				// set gamma
				gameRecRestore.csGTable = (Ptr) pTableGammaNew;				// setup restore record
				csPtr = (Ptr) &gameRecRestore;
				err = Control((**hGD).gdRefNum, cscSetGamma, (Ptr) &csPtr);	// restore gamma (note, display drivers may delay returning from this until VBL)
				
				if ((8 == (**(**hGD).gdPMap).pixelSize) && (noErr == err))	// if successful and on an 8 bit device
				{
					hCTabDeviceColors = (**(**hGD).gdPMap).pmTable;			// do SetEntries to force CLUT update
					setEntriesRec.csTable = (ColorSpec *) &(**hCTabDeviceColors).ctTable;
					setEntriesRec.csStart = 0;
					setEntriesRec.csCount = (**hCTabDeviceColors).ctSize;
					csPtr = (Ptr) &setEntriesRec;
					err = Control((**hGD).gdRefNum, cscSetEntries, (Ptr) &csPtr);	// SetEntries in CLUT
				}
				DisposeGammaTable ((Ptr) pTableGammaNew);					// dump table
				if (noErr == err)
					return true;
			}
		}
	}
	else																	// set NULL gamma -> results in linear map
	{
		gameRecRestore.csGTable = (Ptr) NULL;								// setup restore record
		csPtr = (Ptr) &gameRecRestore;
		err = Control((**hGD).gdRefNum, cscSetGamma, (Ptr) &csPtr);			// restore gamma
		
		if ((8 == (**(**hGD).gdPMap).pixelSize) && (noErr == err))			// if successful and on an 8 bit device
		{
			hCTabDeviceColors = (**(**hGD).gdPMap).pmTable;					// do SetEntries to force CLUT update
			setEntriesRec.csTable = (ColorSpec *) &(**hCTabDeviceColors).ctTable;
			setEntriesRec.csStart = 0;
			setEntriesRec.csCount = (**hCTabDeviceColors).ctSize;
			csPtr = (Ptr) &setEntriesRec;
			err = Control((**hGD).gdRefNum, cscSetEntries, (Ptr) &csPtr);	// SetEntries in CLUT
		}
		if (noErr == err)
			return true;
	}
	return false;															// memory allocation or device control failed if we get here
}

// --------------------------------------------------------------------------

// SetDeviceGammaRampGW

// sets the gamma ramp for a graphics device associated with a GWorld pointer (pRamp: 3 arrays of 256 elements each (R,G,B))

Boolean SetDeviceGammaRampGW (GWorldPtr pGW, Ptr pRamp)
{
	GDHandle hGD = GetGWorldDevice (pGW);
	return SetDeviceGammaRampGD (hGD, pRamp);
}

// --------------------------------------------------------------------------

// SetDeviceGammaRampCGP

// sets the gamma ramp for a graphics device associated with a CGraf pointer (pRamp: 3 arrays of 256 elements each (R,G,B))

Boolean SetDeviceGammaRampCGP (CGrafPtr pGraf, Ptr pRamp)
{
	CGrafPtr pGrafSave;
	GDHandle hGDSave;
	GetGWorld (&pGrafSave, &hGDSave);
	SetGWorld (pGraf, NULL);
	GDHandle hGD = GetGDevice ();
	Boolean fResult = SetDeviceGammaRampGD (hGD, pRamp);
	SetGWorld (pGrafSave, hGDSave);
	return fResult;
}

}
