/*
 * $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);
}