/*
 * $Id: display.c,v 1.57 2004/02/04 16:59:01 phil Exp $
 * Simulator and host O/S independent XY display simulator
 * Phil Budne <phil@ultimate.com>
 * September 2003
 *
 * with changes by Douglas A. Gwyn, 21 Jan. 2004
 *
 * started from PDP-8/E simulator vc8e.c;
 *	This PDP8 Emulator was written by Douglas W. Jones at the
 *	University of Iowa.  It is distributed as freeware, of
 *	uncertain function and uncertain utility.
 */

/*
 * Copyright (c) 2003-2004, Philip L. Budne
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * Except as contained in this notice, the names of the authors shall
 * not be used in advertising or otherwise to promote the sale, use or
 * other dealings in this Software without prior written authorization
 * from the authors.
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <limits.h>			/* for USHRT_MAX */
#include "ws.h"
#include "display.h"

/*
 * The user may select (at compile time) how big a window is used to
 * emulate the display.  Using smaller windows saves memory and screen space.
 *
 * Type 30 has 1024x1024 addressing, but only 512x512 visible points.
 * VR14 has only 1024x768 visible points; VR17 has 1024x1024 visible points.
 * VT11 supports 4096x4096 addressing, clipping to the lowest 1024x1024 region.
 * VR48 has 1024x1024 visible points in the main display area and 128x1024
 * visible points in a menu area on the right-hand side (1152x1024 total).
 * VT48 supports 8192x8192 (signed) main-area addressing, clipping to a
 * 1024x1024 window which can be located anywhere within that region.
 * (XXX -- That is what the VT11/VT48 manuals say; however, evidence suggests
 * that the VT11 may actually support 8192x8192 (signed) addressing too.)
 */

/* Define the default display type (if display_init() not called) */
#ifndef DISPLAY_TYPE
#define DISPLAY_TYPE DIS_TYPE30
#endif /* DISPLAY_TYPE not defined */

/* select a default resolution if display_init() not called */
/* XXX keep in struct display? */
#ifndef PIX_SCALE
#define PIX_SCALE RES_HALF
#endif /* PIX_SCALE not defined */

/* select a default light-pen hit radius if display_init() not called */
#ifndef PEN_RADIUS
#define PEN_RADIUS 4
#endif /* PEN_RADIUS not defined */

/*
 * note: displays can have up to two different colors (eg VR20)
 * each color can be made up of any number of phosphors
 * with different colors and decay characteristics (eg Type 30)
 */

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

struct phosphor {
    double red, green, blue;
    double level;			/* decay level (0.5 for half life) */
    double t_level;			/* seconds to decay to level */
};

struct color {
    struct phosphor *phosphors;
    int nphosphors;
    int half_life;			/* for refresh calc */
};

struct display {
    enum display_type type;
    char *name;
    struct color *color0, *color1;
    short xpoints, ypoints;
};

/*
 * original phosphor constants from Raphael Nabet's XMame 0.72.1 PDP-1 sim.
 * not even sure Type30 really used P17 (guess by Daniel P. B. Smith)
 */
static struct phosphor p17[] = {
    {0.11, 0.11, 1.0,  0.5, 0.05},	/* fast blue */
    {1.0,  1.0,  0.11, 0.5, 0.20}	/* slow yellow/green */
};
static struct color color_p17 = { p17, ELEMENTS(p17), 125000 };

/* green phosphor for VR14, VR17, VR20 */
static struct phosphor p29[] = {{0.0260, 1.0, 0.00121, 0.5, 0.025}};
struct color color_p29 = { p29, ELEMENTS(p29), 25000 };

static struct phosphor p40[] = {
    /* P40 blue-white spot with yellow-green decay (.045s to 10%?) */
    {0.4, 0.2, 0.924, 0.5, 0.0135},
    {0.5, 0.7, 0.076, 0.5, 0.065}
};
static struct color color_p40 = { p40, ELEMENTS(p40), 20000 };

/* "red" -- until real VR20 phosphor type/number/constants known */
static struct phosphor pred[] = { {1.0, 0.37, 0.37, 0.5, 0.10} };
static struct color color_red = { pred, ELEMENTS(pred), 100000 };

static struct display displays[] = {
   /*
     * TX-0
     * 
     *
     * Unknown manufacturer
     * 
     * 12" tube, 
     * maximum dot size ???
     * 50us point plot time (20,000 points/sec)
     * P17 Phosphor??? Two phosphor layers:
     * fast blue (.05s half life), and slow green (.2s half life)
     * 
     * 
     */
    { DIS_TX0, "MIT TX-0", &color_p17, NULL, 512, 512 },

	
	/*
     * Type 30
     * PDP-1/4/5/8/9/10 "Precision CRT" display system
     *
     * Raytheon 16ADP7A CRT?
     * web searches for 16ADP7 finds useful information!!
     * 16" tube, 14 3/8" square raster
     * maximum dot size .015"
     * 50us point plot time (20,000 points/sec)
     * P17 Phosphor??? Two phosphor layers:
     * fast blue (.05s half life), and slow green (.2s half life)
     * 360 lb
     * 7A at 115+-10V 60Hz
     */
    { DIS_TYPE30, "Type 30", &color_p17, NULL, 1024, 1024 },

    /*
     * VR14
     * used w/ GT40/44, AX08, VC8E
     *
     * Viewable area 6.75" x 9"
     * 12" diagonal
     * brightness >= 30 fL
     * dot size .02" (20 mils)
     * settle time:
     *	full screen 18us to +/-1 spot diameter
     *	.1" change 1us to +/-.5 spot diameter
     * weight 75lb
     */
    { DIS_VR14, "VR14", &color_p29, NULL, 1024, 768 },

    /*
     * VR17
     * used w/ GT40/44, AX08, VC8E
     *
     * Viewable area 9.25" x 9.25"
     * 17" diagonal
     * dot size .02" (20 mils)
     * brightness >= 25 fL
     * phosphor: P39 doped for IR light pen use
     * light pen: Type 375
     * weight 85lb
     */
    { DIS_VR17, "VR17", &color_p29, NULL, 1024, 1024 },

    /*
     * VR20
     * on VC8E
     * Two colors!!
     */
    { DIS_VR20, "VR20", &color_p29, &color_red, 1024, 1024 },

    /*
     * VR48
     * (on VT48 in VS60)
     * from Douglas A. Gwyn 23 Nov. 2003
     *
     * Viewable area 12" x 12", plus 1.5" x 12" menu area on right-hand side
     * 21" diagonal
     * dot size <= .01" (10 mils)
     * brightness >= 31 fL
     * phosphor: P40 (blue-white fluorescence with yellow-green phosphorescence)
     * light pen: Type 377A (with tip switch)
     * driving circuitry separate
     * (normally under table on which CRT is mounted)
     */
    { DIS_VR48, "VR48", &color_p40, NULL, 1024+VR48_GUTTER+128, 1024 },

    /*
     * Type 340 Display system
     * on PDP-4/6/7/9/10
     *
     * 1024x1024
     * 9 3/8" raster (.01" dot pitch)
     * 0,0 at lower left
     * 8 intensity levels
     */
    { DIS_TYPE340, "Type 340", &color_p17, NULL, 1024, 1024 }
};

/*
 * Unit time (in microseconds) used to store display point time to
 * live at current aging level.  If this is too small, delay values
 * cannot fit in an unsigned short.  If it is too large all pixels
 * will age at once.  Perhaps a suitable value should be calculated at
 * run time?  When display_init() calculates refresh_interval it
 * sanity checks for both cases.
 */
#define DELAY_UNIT 250

/* levels to display in first half-life; determines refresh rate */
#ifndef LEVELS_PER_HALFLIFE
#define LEVELS_PER_HALFLIFE 4
#endif

/* after 5 half lives (.5**5) remaining intensity is 3% of original */
#ifndef HALF_LIVES_TO_DISPLAY
#define HALF_LIVES_TO_DISPLAY 5
#endif

/*
 * refresh_rate is number of times per (simulated) second a pixel is
 * aged to next lowest intensity level.
 *
 * refresh_rate = ((1e6*LEVELS_PER_HALFLIFE)/PHOSPHOR_HALF_LIFE)
 * refresh_interval = 1e6/DELAY_UNIT/refresh_rate
 *		    = PHOSPHOR_HALF_LIFE/LEVELS_PER_HALF_LIFE
 * intensities = (HALF_LIVES_TO_DISPLAY*PHOSPHOR_HALF_LIFE)/refresh_interval
 *	       = HALF_LIVES_TO_DISPLAY*LEVELS_PER_HALFLIFE
 *
 * See also comments on display_age()
 *
 * Try to keep LEVELS_PER_HALFLIFE*HALF_LIVES_TO_DISPLAY*NLEVELS <= 192
 * to run on 8-bit (256 color) displays!
 */

/*
 * number of aging periods to display a point for
 */
#define NTTL (HALF_LIVES_TO_DISPLAY*LEVELS_PER_HALFLIFE)

/*
 * maximum (initial) TTL for a point.
 * TTL's are stored 1-based
 * (a stored TTL of zero means the point is off)
 */
#define MAXTTL NTTL

/*
 * number of drawing intensity levels
 */
#define NLEVELS (DISPLAY_INT_MAX-DISPLAY_INT_MIN+1)

#define MAXLEVEL (NLEVELS-1)

/*
 * Display Device Implementation
 */

/*
 * Each point on the display is represented by a "struct point".  When
 * a point isn't dark (intensity > 0), it is linked into a circular,
 * doubly linked delta queue (a priority queue where "delay"
 * represents the time difference from the previous entry (if any) in
 * the queue.
 *
 * All points are aged refresh_rate times/second, each time moved to the
 * next (logarithmically) lower intensity level.  When display_age() is
 * called, only the entries which have expired are processed.  Calling
 * display_age() often allows spreading out the workload.
 *
 * An alternative would be to have intensity levels represent linear
 * decreases in intensity, and have the decay time at each level change.
 * Inverting the decay function for a multi-component phosphor may be
 * tricky, and the two different colors would need different time tables.
 * Furthermore, it would require finding the correct location in the
 * queue when adding a point (currently only need to add points at end)
 */

/*
 * 12 bytes/entry on 32-bit system when REFRESH_RATE > 15
 * (requires 3MB for 512x512 display).
 */

typedef unsigned short delay_t;
#define DELAY_T_MAX USHRT_MAX

struct point {
    struct point *next;			/* next entry in queue */
    struct point *prev;			/* prev entry in queue */
    delay_t delay;			/* delta T in DELAY_UNITs */
    unsigned char ttl;			/* zero means off, not linked in */
    unsigned char level : 7;		/* intensity level */
    unsigned char color : 1;		/* for VR20 (two colors) */
};

static struct point *points;		/* allocated array of points */
static struct point _head;
#define head (&_head)

/*
 * time span of all entries in queue
 * should never exceed refresh_interval
 * (should be possible to make this a delay_t)
 */
static long queue_interval;

/* convert X,Y to a "struct point *" */
#define P(X,Y) (points + (X) + ((Y)*(size_t)xpixels))

/* convert "struct point *" to X and Y */
#define X(P) (((P) - points) % xpixels)
#define Y(P) (((P) - points) / xpixels)

static int initialized = 0;

/*
 * global set by O/S display level to indicate "light pen tip switch activated"
 * (This is used only by the VS60 emulation, also by vttest to change patterns)
 */
unsigned char display_lp_sw = 0;

/*
 * global set by DR11-C simulation when DR device enabled; deactivates
 * light pen and instead reports mouse coordinates as Talos digitizer
 * data via DR11-C
 */
unsigned char display_tablet = 0;

/*
 * can be changed with display_lp_radius()
 */
static long scaled_pen_radius_squared;

/* run-time -- set by display_init() */
static int xpoints, ypoints;
static int xpixels, ypixels;
static int refresh_rate;
static int refresh_interval;
static int ncolors;
static enum display_type display_type;
static int scale;

/*
 * relative brightness for each display level
 * (all but last must be less than 1.0)
 */
static float level_scale[NLEVELS];

/*
 * table of pointer to window system "colors"
 * for painting each age level, intensity level and beam color
 */
void *colors[2][NLEVELS][NTTL];

void
display_lp_radius(int r)
{
    r /= scale;
    scaled_pen_radius_squared = r * r;
}

/*
 * from display_age and display_point
 * since all points age at the same rate,
 * only adds points at end of list.
 */
static void
queue_point(struct point *p)
{
    int d;

    d = refresh_interval - queue_interval;
    queue_interval += d;
    /* queue_interval should now be == refresh_interval */

#ifdef PARANOIA
    if (p->ttl == 0 || p->ttl > MAXTTL)
	printf("queuing %d,%d level %d!\n", X(p), Y(p), p->level);
    if (d > DELAY_T_MAX)
	printf("queuing %d,%d delay %d!\n", X(p), Y(p), d);
    if (queue_interval > DELAY_T_MAX)
	printf("queue_interval (%d) > DELAY_T_MAX (%d)\n",
	       (int)queue_interval, DELAY_T_MAX);
#endif /* PARANOIA defined */

    p->next = head;
    p->prev = head->prev;

    head->prev->next = p;
    head->prev = p;

    p->delay = d;
}

/*
 * here to to dynamically adjust interval for examination
 * of elapsed vs. simulated time, and fritter away
 * any extra wall-clock time without eating CPU
 */

/*
 * more parameters!
 */

/*
 * upper bound for elapsed time between elapsed time checks.
 * if more than MAXELAPSED microseconds elapsed while simulating
 * delay_check simulated microseconds, decrease delay_check.
 */
#define MAXELAPSED 100000		/* 10Hz */

/*
 * lower bound for elapsed time between elapsed time checks.
 * if fewer than MINELAPSED microseconds elapsed while simulating
 * delay_check simulated microseconds, increase delay_check.
 */
#define MINELAPSED 50000		/* 20Hz */

/*
 * upper bound for delay (sleep/poll).
 * If difference between elapsed time and simulated time is
 * larger than MAXDELAY microseconds, decrease delay_check.
 *
 * since delay is elapsed time - simulated time, MAXDELAY
 * should be <= MAXELAPSED
 */
#ifndef MAXDELAY
#define MAXDELAY 100000			/* 100ms */
#endif /* MAXDELAY not defined */

/*
 * lower bound for delay (sleep/poll).
 * If difference between elapsed time and simulated time is
 * smaller than MINDELAY microseconds, increase delay_check.
 *
 * since delay is elapsed time - simulated time, MINDELAY
 * should be <= MINELAPSED
 */
#ifndef MINDELAY
#define MINDELAY 50000			/* 50ms */
#endif /* MINDELAY not defined */

/*
 * Initial amount of simulated time to elapse before polling.
 * Value is very low to ensure polling occurs on slow systems.
 * Fast systems should ramp up quickly.
 */
#ifndef INITIAL_DELAY_CHECK
#define INITIAL_DELAY_CHECK 1000	/* 1ms */
#endif /* INITIAL_DELAY_CHECK */

/*
 * gain factor (2**-GAINSHIFT) for adjustment of adjustment
 * of delay_check
 */
#ifndef GAINSHIFT
#define GAINSHIFT 3			/* gain=0.125 (12.5%) */
#endif /* GAINSHIFT not defined */

static void
display_delay(int t, int slowdown)
{
    /* how often (in simulated us) to poll/check for delay */
    static unsigned long delay_check = INITIAL_DELAY_CHECK;

    /* accumulated simulated time */
    static unsigned long sim_time = 0;
    unsigned long elapsed;
    long delay;

    sim_time += t;
    if (sim_time < delay_check)
	return;

    elapsed = os_elapsed();		/* read and reset elapsed timer */
    if (elapsed == ~0L) {		/* first time thru? */
	slowdown = 0;			/* no adjustments */
	elapsed = sim_time;
    }

    /*
     * get delta between elapsed (real) time, and simulated time.
     * if simulated time running faster, we need to slow things down (delay)
     */
    if (slowdown)
	delay = sim_time - elapsed;
    else
	delay = 0;			/* just poll */

#ifdef DEBUG_DELAY2
    printf("sim %d elapsed %d delay %d\r\n", sim_time, elapsed, delay);
#endif

    /*
     * Try to keep the elapsed (real world) time between checks for
     * delay (and poll for window system events) bounded between
     * MAXELAPSED and MINELAPSED.  Also tries to keep
     * delay/poll time bounded between MAXDELAY and MINDELAY -- large
     * delays make the simulation spastic, while very small ones are
     * inefficient (too many system calls) and tend to be inaccurate
     * (operating systems have a certain granularity for time
     * measurement, and when you try to sleep/poll for very short
     * amounts of time, the noise will dominate).
     *
     * delay_check period may be adjusted often, and oscillate.  There
     * is no single "right value", the important things are to keep
     * the delay time and max poll intervals bounded, and responsive
     * to system load.
     */
    if (elapsed > MAXELAPSED || delay > MAXDELAY) {
	/* too much elapsed time passed, or delay too large; shrink interval */
	if (delay_check > 1) {
	    delay_check -= delay_check>>GAINSHIFT;
#ifdef DEBUG_DELAY
	    printf("reduced period to %d\r\n", delay_check);
#endif /* DEBUG_DELAY defined */
	}
    }
    else if ((elapsed < MINELAPSED) || (slowdown && (delay < MINDELAY))) {
	/* too little elapsed time passed, or delta very small */
	int gain = delay_check>>GAINSHIFT;
	if (gain == 0)
	    gain = 1;			/* make sure some change made! */
	delay_check += gain;
#ifdef DEBUG_DELAY
	printf("increased period to %d\r\n", delay_check);
#endif /* DEBUG_DELAY defined */
    }
    if (delay < 0)
	delay = 0;
    /* else if delay < MINDELAY, clamp at MINDELAY??? */

    /* poll for window system events and/or delay */
    ws_poll(NULL, delay);

    sim_time = 0;			/* reset simulated time clock */

    /*
     * delay (poll/sleep) time included in next "elapsed" period
     * (clock not reset after a delay)
     */
} /* display_delay */

/*
 * here periodically from simulator to age pixels.
 *
 * calling often with small values will age a few pixels at a time,
 * and assist with graceful aging of display, and pixel aging.
 *
 * values should be smaller than refresh_interval!
 *
 * returns true if anything on screen changed.
 */

int
display_age(int t,			/* simulated us since last call */
	    int slowdown)		/* slowdown to simulated speed */
{
    struct point *p;
    static int elapsed = 0;
    int changed;

    if (!initialized && !display_init(DISPLAY_TYPE, PIX_SCALE))
	return 0;

    display_delay(t, slowdown);

    changed = 0;

    elapsed += t;
    if (elapsed < DELAY_UNIT)
	return 0;

    t = elapsed / DELAY_UNIT;
    elapsed %= DELAY_UNIT;

    while ((p = head->next) != head) {
	int x, y;

	/* look at oldest entry */
	if (p->delay > t) {		/* further than our reach? */
	    p->delay -= t;		/* update head */
	    queue_interval -= t;	/* update span */
	    break;			/* quit */
	}

	x = X(p);
	y = Y(p);
#ifdef PARANOIA
 	if (p->ttl == 0)
 	    printf("BUG: age %d,%d ttl zero\n", x, y);
#endif /* PARANOIA defined */

	/* dequeue point */
	p->prev->next = p->next;
	p->next->prev = p->prev;

	t -= p->delay;			/* lessen our reach */
	queue_interval -= p->delay;	/* update queue span */

	ws_display_point(x, y, colors[p->color][p->level][--p->ttl]);
	changed = 1;

	/* queue it back up, unless we just turned it off! */
	if (p->ttl > 0)
	    queue_point(p);
    }
    return changed;
} /* display_age */

/* here from window system */
void
display_repaint(void) {
    struct point *p;
    int x, y;
    /*
     * bottom to top, left to right.
     */
    for (p = points, y = 0; y < ypixels; y++)
	for (x = 0; x < xpixels; p++, x++)
	    if (p->ttl)
		ws_display_point(x, y, colors[p->color][p->level][p->ttl-1]);
    ws_sync();
}

/* (0,0) is lower left */
static int
intensify(int x,			/* 0..xpixels */
	  int y,			/* 0..ypixels */
	  int level,			/* 0..MAXLEVEL */
	  int color)			/* for VR20! 0 or 1 */
{
    struct point *p;
    int bleed;

    if (x < 0 || x >= xpixels || y < 0 || y >= ypixels)
	return 0;			/* limit to display */

    p = P(x,y);
    if (p->ttl) {			/* currently lit? */
#ifdef LOUD
	printf("%d,%d old level %d ttl %d new %d\r\n",
	       x, y, p->level, p->ttl, level);
#endif /* LOUD defined */

	/* unlink from delta queue */
	p->prev->next = p->next;

	if (p->next == head)
	    queue_interval -= p->delay;
	else
	    p->next->delay += p->delay;
	p->next->prev = p->prev;
    }

    bleed = 0;				/* no bleeding for now */

    /* EXP: doesn't work... yet */
    /* if "recently" drawn, same or brighter, same color, make even brighter */
    if (p->ttl >= MAXTTL*2/3 && level >= p->level && p->color == color &&
	level < MAXLEVEL)
	level++;

    /*
     * this allows a dim beam to suck light out of
     * a recently drawn bright spot!!
     */
    if (p->ttl != MAXTTL || p->level != level || p->color != color) {
	p->ttl = MAXTTL;
	p->level = level;
	p->color = color;		/* save color even if monochrome */
	ws_display_point(x, y, colors[p->color][p->level][p->ttl-1]);
    }

    queue_point(p);			/* put at end of list */
    return bleed;
}

int
display_point(int x,			/* 0..xpixels (unscaled) */
	      int y,			/* 0..ypixels (unscaled) */
	      int level,		/* DISPLAY_INT_xxx */
	      int color)		/* for VR20! 0 or 1 */
{
    long lx, ly;

    if (!initialized && !display_init(DISPLAY_TYPE, PIX_SCALE))
	return 0;

    /* scale x and y to the displayed number of pixels */
    /* handle common cases quickly */
    if (scale > 1) {
	if (scale == 2) {
	    x >>= 1;
	    y >>= 1;
	}
	else {
	    x /= scale;
	    y /= scale;
	}
    }

#if DISPLAY_INT_MIN > 0
    level -= DISPLAY_INT_MIN;		/* make zero based */
#endif
    intensify(x, y, level, color);
    /* no bleeding for now (used to recurse for neighbor points) */

    if (ws_lp_x == -1 || ws_lp_y == -1)
	return 0;

    lx = x - ws_lp_x;
    ly = y - ws_lp_y;
    return lx*lx + ly*ly <= scaled_pen_radius_squared;
} /* display_point */

/*
 * calculate decay color table for a phosphor mixture
 * must be called AFTER refresh_rate initialized!
 */
static void
phosphor_init(struct phosphor *phosphors, int nphosphors, int color)
{
    int ttl;

    /* for each display ttl level; newest to oldest */
    for (ttl = NTTL-1; ttl > 0; ttl--) {
	struct phosphor *pp;
	double rr, rg, rb;		/* real values */

	/* fractional seconds */
	double t = ((double)(NTTL-1-ttl))/refresh_rate;

	int ilevel;			/* intensity levels */
	int p;

	/* sum over all phosphors in mixture */
	rr = rg = rb = 0.0;
	for (pp = phosphors, p = 0; p < nphosphors; pp++, p++) {
	    double decay = pow(pp->level, t/pp->t_level);
	    rr += decay * pp->red;
	    rg += decay * pp->green;
	    rb += decay * pp->blue;
	}

	/* scale for brightness for each intensity level */
	for (ilevel = MAXLEVEL; ilevel >= 0; ilevel--) {
	     int r, g, b;
	     void *cp;

	     /*
	      * convert to 16-bit integer; clamp at 16 bits.
	      * this allows the sum of brightness factors across phosphors
	      * for each of R G and B to be greater than 1.0
	      */

	     r = (int)(rr * level_scale[ilevel] * 0xffff);
	     if (r > 0xffff) r = 0xffff;

	     g = (int)(rg * level_scale[ilevel] * 0xffff);
	     if (g > 0xffff) g = 0xffff;

	     b = (int)(rb * level_scale[ilevel] * 0xffff);
	     if (b > 0xffff) b = 0xffff;

	     cp = ws_color_rgb(r, g, b);
	     if (!cp) {				/* allocation failed? */
		 if (ttl == MAXTTL-1) {		/* brand new */
		     if (ilevel == MAXLEVEL)	/* highest intensity? */
		         cp = ws_color_white();	/* use white */
		     else
		         cp = colors[color][ilevel+1][ttl]; /* use next lvl */
		 } /* brand new */
		 else if (r + g + b >= 0xffff*3/3) /* light-ish? */
		     cp = colors[color][ilevel][ttl+1]; /* use previous TTL */
		 else
		     cp = ws_color_black();
	     }
	     colors[color][ilevel][ttl] = cp;
	 } /* for each intensity level */
    } /* for each TTL */
} /* phosphor_init */

static struct display *
find_type(enum display_type type)
{
    int i;
    struct display *dp;
    for (i = 0, dp = displays; i < ELEMENTS(displays); i++, dp++)
	if (dp->type == type)
	    return dp;
    return NULL;
}

int
display_init(enum display_type type, int sf)
{
    static int init_failed = 0;
    struct display *dp;
    int half_life;
    int i;

    if (initialized) {
	/* cannot change type once started */
	/* XXX say something???? */
	return type == display_type;
    }

    if (init_failed)
	return 0;			/* avoid thrashing */

    init_failed = 1;			/* assume the worst */
    dp = find_type(type);
    if (!dp) {
	fprintf(stderr, "Unknown display type %d\r\n", (int)type);
	goto failed;
    }

    /* Initialize display list */
    head->next = head->prev = head;

    display_type = type;
    scale = sf;

    xpoints = dp->xpoints;
    ypoints = dp->ypoints;

    /* increase scale factor if won't fit on desktop? */
    xpixels = xpoints / scale;
    ypixels = ypoints / scale;

    /* set default pen radius now that scale is set */
    display_lp_radius(PEN_RADIUS);

    ncolors = 1;
    /*
     * use function to calculate from looking at avg (max?)
     * of phosphor half lives???
     */
#define COLOR_HALF_LIFE(C) ((C)->half_life)

    half_life = COLOR_HALF_LIFE(dp->color0);
    if (dp->color1) {
	if (dp->color1->half_life > half_life)
	    half_life = COLOR_HALF_LIFE(dp->color1);
	ncolors++;
    }

    /* before phosphor_init; */
    refresh_rate = (1000000*LEVELS_PER_HALFLIFE)/half_life;
    refresh_interval = 1000000/DELAY_UNIT/refresh_rate;

    /*
     * sanity check refresh_interval
     * calculating/selecting DELAY_UNIT at runtime might avoid this!
     */

    /* must be non-zero; interval of 1 means all pixels will age at once! */
    if (refresh_interval < 1) {
	/* decrease DELAY_UNIT? */
	fprintf(stderr, "NOTE! refresh_interval too small: %d\r\n",
		refresh_interval);

	/* dunno if this is a good idea, but might be better than dying */
	refresh_interval = 1;
    }

    /* point lifetime in DELAY_UNITs will not fit in p->delay field! */
    if (refresh_interval > DELAY_T_MAX) {
	/* increase DELAY_UNIT? */
	fprintf(stderr, "bad refresh_interval %d > DELAY_T_MAX %d\r\n",
		refresh_interval, DELAY_T_MAX);
	goto failed;
    }

    /*
     * before phosphor_init;
     * set up relative brightness of display intensity levels
     * (could differ for different hardware)
     *
     * linear for now.  boost factor insures low intensities are visible
     */
#define BOOST 5
    for (i = 0; i < NLEVELS; i++)
	level_scale[i] = ((float)i+1+BOOST)/(NLEVELS+BOOST);

    points = (struct point *)calloc((size_t)xpixels,
				    ypixels * sizeof(struct point));
    if (!points)
	goto failed;

    if (!ws_init(dp->name, xpixels, ypixels, ncolors))
	goto failed;

    phosphor_init(dp->color0->phosphors, dp->color0->nphosphors, 0);

    if (dp->color1)
	phosphor_init(dp->color1->phosphors, dp->color1->nphosphors, 1);

    initialized = 1;
    init_failed = 0;			/* hey, we made it! */
    return 1;

 failed:
    fprintf(stderr, "Display initialization failed\r\n");
    return 0;
}

void
display_reset(void)
{
    /* XXX tear down window? just clear it? */
}

void
display_sync(void)
{
    ws_sync();
}

void
display_beep(void)
{
    ws_beep();
}

int
display_xpoints(void)
{
    return xpoints;
}

int
display_ypoints(void)
{
    return ypoints;
}

int
display_scale(void)
{
    return scale;
}

/*
 * handle keyboard events
 *
 * data switches; 18 -- enough for PDP-1/4/7/9/15 (for munching squares!)
 * 123 456 789 qwe rty uio
 * bit toggled on key up
 * all cleared on space
 *
 * spacewar switches; bit high as long as key down
 * asdf kl;'
 * just where PDP-1 spacewar expects them!
 * key mappings same as MIT Media Lab Java PDP-1 simulator
 * 
 */
unsigned long spacewar_switches = 0;

/* here from window system */
void
display_keydown(int k)
{
    switch (k) {
    case 'f': case 'F': spacewar_switches |= 01; break; /* torpedos */
    case 'd': case 'D': spacewar_switches |= 02; break; /* engines */
    case 'a': case 'A': spacewar_switches |= 04; break; /* rotate R */
    case 's': case 'S': spacewar_switches |= 010; break; /* rotate L */
    case '\'': case '"': spacewar_switches |= 040000; break; /* torpedos */
    case ';': case ':':	spacewar_switches |= 0100000; break; /* engines */
    case 'k': case 'K': spacewar_switches |= 0200000; break; /* rotate R */
    case 'l': case 'L': spacewar_switches |= 0400000; break; /* rotate L */
    default: return;
    }
}

/* here from window system */
void
display_keyup(int k)
{
    unsigned long test_switches = cpu_get_switches();

    /* fetch console switches from simulator? */
    switch (k) {
    case 'f': case 'F': spacewar_switches &= ~01; return;
    case 'd': case 'D': spacewar_switches &= ~02; return;
    case 'a': case 'A': spacewar_switches &= ~04; return;
    case 's': case 'S': spacewar_switches &= ~010; return;

    case '\'': case '"': spacewar_switches &= ~040000; return;
    case ';': case ':': spacewar_switches &= ~0100000; return;
    case 'k': case 'K': spacewar_switches &= ~0200000; return;
    case 'l': case 'L': spacewar_switches &= ~0400000; return;

    case '1': test_switches ^= 1<<17; break;
    case '2': test_switches ^= 1<<16; break;
    case '3': test_switches ^= 1<<15; break;

    case '4': test_switches ^= 1<<14; break;
    case '5': test_switches ^= 1<<13; break;
    case '6': test_switches ^= 1<<12; break;

    case '7': test_switches ^= 1<<11; break;
    case '8': test_switches ^= 1<<10; break;
    case '9': test_switches ^= 1<<9; break;

    case 'q': case 'Q': test_switches ^= 1<<8; break;
    case 'w': case 'W': test_switches ^= 1<<7; break;
    case 'e': case 'E': test_switches ^= 1<<6; break;

    case 'r': case 'R': test_switches ^= 1<<5; break;
    case 't': case 'T': test_switches ^= 1<<4; break;
    case 'y': case 'Y': test_switches ^= 1<<3; break;

    case 'u': case 'U': test_switches ^= 1<<2; break;
    case 'i': case 'I': test_switches ^= 1<<1; break;
    case 'o': case 'O': test_switches ^= 1; break;

    case ' ': test_switches = 0; break;
    default: return;
    }
    cpu_set_switches(test_switches);
}