/*
 * cybercube.c -- "CYBER CUBE"  -  a 90s style demo for the 386SX
 *
 * Plays a RAD v1.0 module (track.h) on an AdLib/OPL2/OPL3 card while an
 * svgalib animation runs in 320x200x256 (CL-GD542x or any VGA):
 *
 *    - a small gradient cube (blue -> green -> yellow) spins in the middle,
 *    - it bounces around the screen,
 *    - it morphs into a green "cyber ball" and
 *    - matrix-style characters rain down behind it.  Then it loops.
 *
 * A green terminal-style bar at the bottom scrolls the message right to left.
 *
 * Built for a 386SX-40 with no FPU:  NO floating point anywhere, only
 * 16.16 / 14-bit fixed point integers, and everything is a pure function
 * of the elapsed time, so the speed does not depend on the frame rate.
 * The music is ticked at 50 Hz from a SIGALRM timer, independent of the
 * drawing speed.
 *
 * Plain C89 for gcc 2.7.2.3 (Slackware 4.0).  Build:
 *
 *     gcc -O2 -m386 -fomit-frame-pointer -o cybercube cybercube.c -lvga
 *
 * Run as root (OPL ports + svgalib):
 *
 *     ./cybercube [-p port] [-v] [-f] [-m] [-h]
 *        -p port  OPL base port in hex (default 388)
 *        -v       wait for vertical retrace (no tearing, lower fps)
 *        -f       skip OPL chip detection
 *        -m       mute: no music, no OPL access
 *     ESC or q quits.  The average frame rate is printed on exit.
 *
 * To use your own tune:  gcc -o rad2h rad2h.c && ./rad2h my.rad > track.h
 * (any RAD v1.0 module made with the Reality Adlib Tracker 1.x).
 */

#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>
#if defined(__GLIBC__)
#include <sys/io.h>
#else
#include <asm/io.h>
extern int ioperm(unsigned long from, unsigned long num, int turn_on);
#endif
#include <vga.h>

#include "track.h"                      /* static const unsigned char rad_module[] */

typedef unsigned char  u8;
typedef unsigned short u16;
typedef unsigned int   u32;             /* int is 32 bits on the i386 */

static volatile int quit = 0;

/* ================================================================== */
/*  OPL access and the RAD v1.0 player                                 */
/* ================================================================== */

static int opl_port = 0x388;

/* bank 0 = ports base+0/+1, bank 1 (OPL3 only) = ports base+2/+3 */
static void opl_out_bank(int bank, int reg, int val)
{
    int i, base = opl_port + (bank ? 2 : 0);

    outb((unsigned char)reg, base);
    for (i = 0; i < 6; i++)
        inb(opl_port);                  /* ~3.3 us */
    outb((unsigned char)val, base + 1);
    for (i = 0; i < 35; i++)
        inb(opl_port);                  /* ~23 us */
}

static void opl_out(int reg, int val)
{
    opl_out_bank(0, reg, val);
}

static int opl_detect(void)
{
    int s1, s2;

    opl_out(0x04, 0x60);
    opl_out(0x04, 0x80);
    s1 = inb(opl_port) & 0xE0;
    opl_out(0x02, 0xFF);
    opl_out(0x04, 0x21);
    usleep(20000);
    s2 = inb(opl_port) & 0xE0;
    opl_out(0x04, 0x60);
    opl_out(0x04, 0x80);
    return (s1 == 0x00 && s2 == 0xC0);
}

/* Put the chip into a known state.  IMPORTANT: the second register bank must
   be reset as well.  If something left the chip in OPL3 mode (the kernel's
   sound driver at boot, or the undefined power-on state), voices without the
   left/right output bits are silent -- and RAD instruments don't set them.
   So switch back to plain OPL2-compatible mode first, then clear everything. */
static void opl_reset(void)
{
    int r;

    opl_out_bank(1, 0x05, 0x00);        /* OPL3 "NEW" bit off: OPL2 mode */
    opl_out_bank(1, 0x04, 0x00);        /* no 4-operator voices */
    opl_out(0x04, 0x80);                /* reset timer flags */
    opl_out(0x08, 0x00);                /* composite sine / note select */
    for (r = 0x20; r <= 0xF5; r++) {    /* key off, silence, zero both banks */
        opl_out(r, 0);
        opl_out_bank(1, r, 0);
    }
    opl_out_bank(1, 0x05, 0x00);        /* make sure NEW stayed off */
    opl_out(0x01, 0x20);                /* waveform select enable */
}

/* ---- Reality AdLib Tracker v1.0 (port of Shayde's PLAYER.ASM 1.1a) ---- */

#define RAD_FSTART 0x156
#define RAD_FRANGE 0x158                /* 0x2AE - 0x156 */

static const int rad_notefreq[12] = {   /* notes 1..12 = C# .. C(next oct) */
    0x16B, 0x181, 0x198, 0x1B0, 0x1CA, 0x1E5,
    0x202, 0x220, 0x241, 0x263, 0x287, 0x2AE
};
static const int rad_choff[9] = { 0x20, 0x21, 0x22, 0x28, 0x29, 0x2A, 0x30, 0x31, 0x32 };

static const u8 *rd_b = rad_module;
static int rd_len = (int)sizeof(rad_module);
static int rd_inst[31];                 /* file offset of 11-byte instrument */
static int rd_orderlist, rd_patlist, rd_patpos;
static int rd_nord, rd_orderpos, rd_speed, rd_speedcnt, rd_line, rd_slow;
static u8  rd_old43[9], rd_oldA0[9], rd_oldB0[9], rd_tsspeed[9];
static u8  rd_tone[9], rd_port[9], rd_vol[9];
static unsigned rd_tsfreq[9];

static unsigned rd16(const u8 *p) { return (unsigned)p[0] | ((unsigned)p[1] << 8); }

/* Returns NULL if the module is a sane RAD v1.0 file, else a message. */
static const char *rad_validate(void)
{
    const u8 *b = rd_b;
    int pos, n, nord, i, o, patlist, off;

    if (rd_len < 40 || memcmp(b, "RAD by REALiTY!!", 16) != 0)
        return "not a RAD module";
    if (b[16] != 0x10)
        return "only RAD version 1.0 modules are supported";
    pos = 18;
    if (b[17] & 0x80) {
        while (pos < rd_len && b[pos])
            pos++;
        if (pos >= rd_len)
            return "truncated description";
        pos++;
    }
    for (;;) {
        if (pos >= rd_len)
            return "truncated instrument list";
        n = b[pos++];
        if (n == 0)
            break;
        if (n > 31 || pos + 11 > rd_len)
            return "corrupt instrument data";
        pos += 11;
    }
    if (pos >= rd_len)
        return "truncated order list";
    nord = b[pos++];
    if (nord == 0)
        return "empty order list";
    patlist = pos + nord;
    if (patlist + 64 > rd_len)
        return "truncated pattern table";
    for (i = 0; i < nord; i++) {
        o = b[pos + i];
        if ((o & 0x80) ? ((o & 0x7F) >= nord) : (o >= 32))
            return "corrupt order list";
    }
    for (i = 0; i < 32; i++) {
        off = (int)rd16(b + patlist + 2 * i);
        if (off != 0 && (off < patlist + 64 || off >= rd_len))
            return "corrupt pattern offset";
    }
    return NULL;
}

static void rad_setvol(int ch, int vol) /* vol: 0..255 byte, >=64 -> 63 */
{
    if (vol >= 64)
        vol = 63;
    rd_old43[ch] = (u8)((rd_old43[ch] & 0xC0) | (vol ^ 0x3F));
    opl_out(rad_choff[ch] + 0x23, rd_old43[ch]);
}

/* instrument: 0 car20 1 mod20 2 car40 3 mod40 4 car60 5 mod60 6 car80
   7 mod80 8 C0 9 carE0 10 modE0 */
static void rad_loadinst(int ch, int inst)
{
    const u8 *d;
    int p, off;

    if (inst < 1 || inst > 31)
        return;
    p = rd_inst[inst - 1];
    if (!p)
        return;
    d = rd_b + p;
    off = rad_choff[ch] - 0x20;
    rd_old43[ch] = d[2];
    opl_out(0x20 + off, d[1]);
    opl_out(0x23 + off, d[0]);
    opl_out(0x40 + off, d[3]);
    opl_out(0x43 + off, d[2]);
    opl_out(0x60 + off, d[5]);
    opl_out(0x63 + off, d[4]);
    opl_out(0x80 + off, d[7]);
    opl_out(0x83 + off, d[6]);
    opl_out(0xE0 + off, d[10]);
    opl_out(0xE3 + off, d[9]);
    opl_out(0xC0 + ch, d[8]);
}

static unsigned rad_getfreq(int ch)
{
    unsigned cx, oct;

    cx = (unsigned)rd_oldA0[ch] | ((unsigned)(rd_oldB0[ch] & 3) << 8);
    oct = (unsigned)((rd_oldB0[ch] >> 2) & 7);
    cx = (cx - RAD_FSTART) & 0xFFFF;
    return (oct * RAD_FRANGE + cx) & 0xFFFF;
}

static void rad_setfreq(int ch, unsigned ax)
{
    unsigned oct, fr;
    int b0;

    ax &= 0xFFFF;
    oct = ax / RAD_FRANGE;
    fr = ax % RAD_FRANGE + RAD_FSTART;
    b0 = (rd_oldB0[ch] & 0xE0) | (int)((oct << 2) & 0xFF) | (int)(fr >> 8);
    rd_oldB0[ch] = (u8)b0;
    opl_out(0xB0 + ch, b0);
    rd_oldA0[ch] = (u8)(fr & 0xFF);
    opl_out(0xA0 + ch, (int)(fr & 0xFF));
}

/* Returns the line to jump to (0..63) or -1. */
static int rad_playnote(int ch, int nb, int ib, int param)
{
    int cmd = ib & 15, note, inst, f, b0;

    if (nb != 0) {
        if (cmd == 3) {                 /* tone slide: note is the target */
            note = (nb & 15) - 1;
            if (note >= 0 && note < 12) {
                rd_tsfreq[ch] = (unsigned)(((nb >> 4) & 7) * RAD_FRANGE
                                + rad_notefreq[note] - RAD_FSTART) & 0xFFFF;
                rd_tone[ch] = 1;
                if (param)
                    rd_tsspeed[ch] = (u8)param;
            }
            return -1;
        }
        rd_oldB0[ch] &= (u8)~0x20;      /* key off previous note */
        opl_out(0xB0 + ch, rd_oldB0[ch]);
        inst = ((nb & 0x80) ? 16 : 0) | (ib >> 4);
        if (inst)
            rad_loadinst(ch, inst);
        note = nb & 15;
        if (note >= 1 && note <= 12) {
            f = rad_notefreq[note - 1];
            b0 = 0x20 | (((nb >> 4) & 7) << 2) | (f >> 8);
            rd_oldB0[ch] = (u8)b0;
            rd_oldA0[ch] = (u8)(f & 0xFF);
            opl_out(0xA0 + ch, f & 0xFF);
            opl_out(0xB0 + ch, b0);
        }
    }

    switch (cmd) {
    case 1:                             /* portamento up */
        rd_port[ch] = (u8)param;
        break;
    case 2:                             /* portamento down */
        rd_port[ch] = (u8)(-param);
        break;
    case 3:                             /* tone slide, no note */
        if (param)
            rd_tsspeed[ch] = (u8)param;
        rd_tone[ch] = 1;
        break;
    case 5:                             /* tone slide + volume slide */
    case 10:                            /* volume slide: <50 down, >50 up */
        rd_vol[ch] = (u8)((param >= 50) ? -(param - 50) : param);
        if (cmd == 5)
            rd_tone[ch] = 1;
        break;
    case 12:                            /* set volume */
        rad_setvol(ch, param);
        break;
    case 13:                            /* jump to line in next pattern */
        if (param < 64)
            return param;
        break;
    case 15:                            /* set speed */
        rd_speed = param;
        break;
    default:
        break;
    }
    return -1;
}

static void rad_update(void)
{
    int ch, vol, spd, s;
    unsigned ax, cx;

    for (ch = 0; ch < 9; ch++) {
        if (rd_port[ch]) {
            ax = rad_getfreq(ch);
            ax = (ax + (unsigned)(int)(signed char)rd_port[ch]) & 0xFFFF;
            rad_setfreq(ch, ax);
        }
        s = (signed char)rd_vol[ch];
        if (s != 0) {
            vol = (rd_old43[ch] & 0x3F) ^ 0x3F;
            vol -= s;
            if (vol < 0)
                vol = 0;
            if (vol > 63)
                vol = 63;
            rad_setvol(ch, vol);
        }
        if (rd_tone[ch]) {
            spd = (signed char)rd_tsspeed[ch];
            ax = rad_getfreq(ch);
            cx = rd_tsfreq[ch];
            if (ax < cx) {
                ax = (ax + (unsigned)spd) & 0xFFFF;
                if (ax >= cx) {
                    ax = cx;
                    rd_tone[ch] = 0;
                }
            } else if (ax > cx) {
                ax = (ax - (unsigned)spd) & 0xFFFF;
                if (ax <= cx) {
                    ax = cx;
                    rd_tone[ch] = 0;
                }
            } else {
                rd_tone[ch] = 0;
            }
            rad_setfreq(ch, ax);
        }
    }
}

/* Move to order position 'pos' following jump markers; the tune loops. */
static void rad_goto_order(int pos)
{
    int pat = 0, guard = 0;

    for (;;) {
        if (pos < 0 || pos >= rd_nord)
            pos = 0;                    /* end of tune: loop */
        pat = rd_b[rd_orderlist + pos];
        if (!(pat & 0x80))
            break;
        pos = pat & 0x7F;
        if (++guard > 300) {
            pat = 0;
            pos = 0;
            break;
        }
    }
    rd_orderpos = pos;
    rd_patpos = (pat < 32) ? (int)rd16(rd_b + rd_patlist + pat * 2) : 0;
}

/* One timer tick (PlayMusic): called 50 times a second from SIGALRM. */
static void rad_tick(void)
{
    int i, ch, nb, ib, par, last, lastline, id, jump = -1, j, bad;
    int p;

    if (rd_speedcnt != 0) {
        rd_speedcnt--;
        rad_update();
        return;
    }
    for (i = 0; i < 9; i++) {
        rd_port[i] = 0;
        rd_vol[i] = 0;
        rd_tone[i] = 0;
    }

    p = rd_patpos;
    if (p > 0 && p < rd_len && (rd_b[p] & 0x7F) == rd_line) {
        lastline = rd_b[p] & 0x80;
        p++;
        for (;;) {
            if (p + 3 > rd_len) {
                rd_patpos = 0;
                break;
            }
            ch = rd_b[p] & 0x7F;
            last = rd_b[p] & 0x80;
            nb = rd_b[p + 1];
            ib = rd_b[p + 2];
            p += 3;
            par = 0;
            if (ib & 15) {
                if (p >= rd_len) {
                    rd_patpos = 0;
                    break;
                }
                par = rd_b[p++];
            }
            if (ch < 9) {
                j = rad_playnote(ch, nb, ib, par);
                if (j >= 0) {
                    jump = j;
                    break;
                }
            }
            if (last) {
                rd_patpos = lastline ? 0 : p;
                break;
            }
        }
    }

    if (jump >= 0) {                    /* Dxx: line 'jump' of next pattern */
        rd_speedcnt = rd_speed & 0xFF;
        rd_line = jump;
        rad_goto_order(rd_orderpos + 1);
        p = rd_patpos;
        if (p != 0) {
            bad = 0;
            for (;;) {                  /* find first line >= target */
                if (p >= rd_len) { bad = 1; break; }
                id = rd_b[p] & 0x7F;
                if (id >= jump)
                    break;
                if (rd_b[p] & 0x80) { bad = 1; break; }
                p++;
                for (;;) {              /* skip this line's channel data */
                    if (p + 3 > rd_len) { bad = 1; break; }
                    last = rd_b[p] & 0x80;
                    par = rd_b[p + 2] & 15;
                    p += 3;
                    if (par)
                        p++;
                    if (last)
                        break;
                }
                if (bad)
                    break;
            }
            rd_patpos = bad ? 0 : p;
        }
        rad_update();
        return;
    }

    rd_speedcnt = (rd_speed - 1) & 0xFF;        /* after effects: new speed */
    rd_line++;
    if (rd_line >= 64) {
        rd_line = 0;
        rad_goto_order(rd_orderpos + 1);
    }
    rad_update();
}

/* Parse the (validated) module and start from the first pattern. */
static void rad_start(void)
{
    const u8 *b = rd_b;
    int flags, pos, n, i;

    flags = b[17];
    rd_slow = (flags & 0x40) != 0;
    rd_speed = flags & 0x1F;
    pos = 18;
    if (flags & 0x80) {                 /* skip description */
        while (pos < rd_len && b[pos])
            pos++;
        pos++;
    }
    for (i = 0; i < 31; i++)
        rd_inst[i] = 0;
    while (pos < rd_len) {
        n = b[pos++];
        if (n == 0)
            break;
        if (n <= 31)
            rd_inst[n - 1] = pos;
        pos += 11;
    }
    rd_nord = b[pos++];
    rd_orderlist = pos;
    rd_patlist = pos + rd_nord;

    memset(rd_old43, 0, sizeof(rd_old43));
    memset(rd_oldA0, 0, sizeof(rd_oldA0));
    memset(rd_oldB0, 0, sizeof(rd_oldB0));
    memset(rd_tsspeed, 1, sizeof(rd_tsspeed));
    memset(rd_tone, 0, sizeof(rd_tone));
    memset(rd_port, 0, sizeof(rd_port));
    memset(rd_vol, 0, sizeof(rd_vol));
    for (i = 0; i < 9; i++)
        rd_tsfreq[i] = 0;
    rd_speedcnt = 0;
    rd_line = 0;
    rd_patpos = 0;
    rad_goto_order(0);
}

/* ---- 50 Hz (or 18.2 Hz "slow timer") music tick from SIGALRM ---- */

static void on_alarm(int sig)
{
    (void)sig;
    rad_tick();
}

static void timer_start(void)
{
    struct sigaction sa;
    struct itimerval it;

    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = on_alarm;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGALRM, &sa, NULL);
    it.it_interval.tv_sec = 0;
    it.it_interval.tv_usec = rd_slow ? 54925 : 20000;
    it.it_value = it.it_interval;
    setitimer(ITIMER_REAL, &it, NULL);
}

static void timer_stop(void)
{
    struct itimerval it;

    memset(&it, 0, sizeof(it));
    setitimer(ITIMER_REAL, &it, NULL);
}

/* ================================================================== */
/*  Graphics: 320x200x256, integer maths only                          */
/* ================================================================== */

#define SW 320
#define SH 200

/* The bottom of the screen is a terminal-style bar with the scroller. */
#define BAR_TOP    168                  /* bar: rows 168..199, play area above */
#define TXT_Y      171                  /* top row of the scroller text */
#define TXT_SCALE  3                    /* 5x7 font magnified 3x */
#define STRIP_H    (8 * TXT_SCALE + 3)  /* glyph rows + 2 px glow + 1 spare */
#define SCROLL_PXS 75                   /* scroll speed, pixels per second */

/* palette layout */
#define PAL_RAIN  16                    /* 16..47: 32 rain greens        */
#define PAL_HEAD  48                    /* bright drop head              */
#define PAL_MESH  64                    /* 64..127: cube / ball colours  */
#define PAL_LINE 128                    /* 128..143: wire-frame greens   */
#define PAL_TEXT 144                    /* 144 glow, 145 text, 146 bar line */
#define LINE_MORPH 128                  /* morph value where the wire-frame appears */

#ifndef STAT
#define STAT(x)                         /* statistics hook (test builds) */
#endif

static u8 fb[SW * SH];                  /* off-screen frame buffer */
static u8 *vgamem;

/* Rows of the play area touched by the previous / current frame: only those
   are cleared and copied to the slow ISA video memory (the cube alone covers
   ~40% of the rows).  The bar at the bottom is redrawn and copied every frame. */
static int prev_lo = 0, prev_hi = BAR_TOP - 1, cur_lo = 0, cur_hi = BAR_TOP - 1;

/* ---- sine: quarter wave, 14-bit (16384 = 1.0), 1024 units per turn ---- */
static const short qsin[257] = {
    0, 101, 201, 302, 402, 503, 603, 704, 804, 904, 1005, 1105,
    1205, 1306, 1406, 1506, 1606, 1706, 1806, 1906, 2006, 2105, 2205, 2305,
    2404, 2503, 2603, 2702, 2801, 2900, 2999, 3098, 3196, 3295, 3393, 3492,
    3590, 3688, 3786, 3883, 3981, 4078, 4176, 4273, 4370, 4467, 4563, 4660,
    4756, 4852, 4948, 5044, 5139, 5235, 5330, 5425, 5520, 5614, 5708, 5803,
    5897, 5990, 6084, 6177, 6270, 6363, 6455, 6547, 6639, 6731, 6823, 6914,
    7005, 7096, 7186, 7276, 7366, 7456, 7545, 7635, 7723, 7812, 7900, 7988,
    8076, 8163, 8250, 8337, 8423, 8509, 8595, 8680, 8765, 8850, 8935, 9019,
    9102, 9186, 9269, 9352, 9434, 9516, 9598, 9679, 9760, 9841, 9921, 10001,
    10080, 10159, 10238, 10316, 10394, 10471, 10549, 10625, 10702, 10778, 10853, 10928,
    11003, 11077, 11151, 11224, 11297, 11370, 11442, 11514, 11585, 11656, 11727, 11797,
    11866, 11935, 12004, 12072, 12140, 12207, 12274, 12340, 12406, 12472, 12537, 12601,
    12665, 12729, 12792, 12854, 12916, 12978, 13039, 13100, 13160, 13219, 13279, 13337,
    13395, 13453, 13510, 13567, 13623, 13678, 13733, 13788, 13842, 13896, 13949, 14001,
    14053, 14104, 14155, 14206, 14256, 14305, 14354, 14402, 14449, 14497, 14543, 14589,
    14635, 14680, 14724, 14768, 14811, 14854, 14896, 14937, 14978, 15019, 15059, 15098,
    15137, 15175, 15213, 15250, 15286, 15322, 15357, 15392, 15426, 15460, 15493, 15525,
    15557, 15588, 15619, 15649, 15679, 15707, 15736, 15763, 15791, 15817, 15843, 15868,
    15893, 15917, 15941, 15964, 15986, 16008, 16029, 16049, 16069, 16088, 16107, 16125,
    16143, 16160, 16176, 16192, 16207, 16221, 16235, 16248, 16261, 16273, 16284, 16295,
    16305, 16315, 16324, 16332, 16340, 16347, 16353, 16359, 16364, 16369, 16373, 16376,
    16379, 16381, 16383, 16384, 16384
};

static int isin(int a)
{
    a &= 1023;
    if (a < 256)
        return qsin[a];
    if (a < 512)
        return qsin[512 - a];
    if (a < 768)
        return -qsin[a - 512];
    return -qsin[1024 - a];
}
#define icos(a) isin((a) + 256)

static int isqrt(int n)
{
    int r = 0;
    while ((r + 1) * (r + 1) <= n)
        r++;
    return r;
}

static int smooth(int x)                /* smoothstep, 0..256 -> 0..256 */
{
    if (x <= 0)
        return 0;
    if (x >= 256)
        return 256;
    return (((x * x) >> 8) * (768 - 2 * x)) >> 8;
}

static u32 rnd_state = 0x1234567u;
static int rnd(void)
{
    rnd_state = rnd_state * 1103515245u + 12345u;
    return (int)((rnd_state >> 16) & 0x7FFF);
}

/* ---- palettes ---- */

static u8 gpal[64][3];                  /* blue -> green -> yellow */
static u8 spal[64][3];                  /* ball: dark -> lit green */

static void init_palettes(void)
{
    int i, j;

    for (i = 0; i < 64; i++) {
        if (i < 32) {
            gpal[i][0] = 0;
            gpal[i][1] = (u8)(10 + (53 * i) / 31);
            gpal[i][2] = (u8)(63 - (57 * i) / 31);
        } else {
            j = i - 32;
            gpal[i][0] = (u8)((63 * j) / 31);
            gpal[i][1] = 63;
            gpal[i][2] = (u8)(6 - (6 * j) / 31);
        }
        spal[i][0] = (u8)(i / 16);
        spal[i][1] = (u8)(2 + (44 * i) / 63);
        spal[i][2] = (u8)(i / 8);
    }
}

static int want_morph = 0, want_rain = 0;       /* set by render_frame() */
static int pal_morph = -1, pal_rain = -1, pal_line = -1, pal_text = 0;

/* Palette changes are applied after the frame has been shown. */
static void apply_palettes(void)
{
    int i, k, m = want_morph, lv = want_rain;

    if (!pal_text) {                    /* phosphor green terminal text, set once */
        pal_text = 1;
        vga_setpalette(PAL_TEXT, 0, 20, 0);
        vga_setpalette(PAL_TEXT + 1, 22, 63, 22);
        vga_setpalette(PAL_TEXT + 2, 0, 38, 0);
    }

    if (m != pal_morph) {               /* cube colours -> ball colours */
        pal_morph = m;
        for (i = 0; i < 64; i++)
            vga_setpalette(PAL_MESH + i,
                           (gpal[i][0] * (256 - m) + spal[i][0] * m) >> 8,
                           (gpal[i][1] * (256 - m) + spal[i][1] * m) >> 8,
                           (gpal[i][2] * (256 - m) + spal[i][2] * m) >> 8);
    }
    k = 0;                              /* wire-frame fades in with the ball */
    if (m >= LINE_MORPH)
        k = 100 + ((m - LINE_MORPH) * 156) / (256 - LINE_MORPH);
    if (k != pal_line) {
        pal_line = k;
        for (i = 0; i < 16; i++)
            vga_setpalette(PAL_LINE + i, (i * k) >> 8,
                           ((24 + (i * 5) / 2) * k) >> 8, ((4 + i) * k) >> 8);
    }
    if (lv != pal_rain) {               /* rain fades in and out */
        pal_rain = lv;
        for (i = 0; i < 32; i++)
            vga_setpalette(PAL_RAIN + i, 0, ((6 + (57 * i) / 31) * lv) >> 8,
                           (((i * i) / 150) * lv) >> 8);
        vga_setpalette(PAL_HEAD, (40 * lv) >> 8, (63 * lv) >> 8, (40 * lv) >> 8);
    }
}

/* ---- matrix rain ---- */

#define NGLYPH 64
#define RCOLS  40                       /* 320 / 8 */
#define RROWS  (BAR_TOP / 8)            /* 21 rows above the bar */

static u8 glyph[NGLYPH][8];
static u8 r_speed[RCOLS], r_trail[RCOLS], r_period[RCOLS], r_off[RCOLS];
static int r_fade[RCOLS];

static void gl_pixel(u8 *g, int x, int y)
{
    if (x >= 0 && x < 7 && y >= 0 && y < 7)
        g[y] |= (u8)(0x80 >> x);
}

static void gl_line(u8 *g, int x0, int y0, int x1, int y1)
{
    int dx = (x1 > x0) ? x1 - x0 : x0 - x1;
    int dy = (y1 > y0) ? y0 - y1 : y1 - y0;     /* negative |dy| */
    int sx = (x0 < x1) ? 1 : -1, sy = (y0 < y1) ? 1 : -1;
    int err = dx + dy, e2;

    for (;;) {
        gl_pixel(g, x0, y0);
        if (x0 == x1 && y0 == y1)
            break;
        e2 = 2 * err;
        if (e2 >= dy) {
            err += dy;
            x0 += sx;
        }
        if (e2 <= dx) {
            err += dx;
            y0 += sy;
        }
    }
}

/* Katakana-ish 7x7 glyphs: a few random strokes each. */
static void init_glyphs(void)
{
    int n, s, k, x, y, len, ns;

    memset(glyph, 0, sizeof(glyph));
    for (n = 0; n < NGLYPH; n++) {
        ns = 3 + rnd() % 3;
        for (s = 0; s < ns; s++) {
            k = rnd() % 5;
            x = rnd() % 4;
            y = rnd() % 4;
            len = 2 + rnd() % 4;
            switch (k) {
            case 0:                     /* horizontal bar */
                gl_line(glyph[n], x, y, x + len, y);
                break;
            case 1:                     /* vertical bar */
                gl_line(glyph[n], x, y, x, y + len);
                break;
            case 2:                     /* diagonal \ */
                gl_line(glyph[n], x, y, x + len, y + len);
                break;
            case 3:                     /* diagonal / */
                gl_line(glyph[n], x + len, y, x, y + len);
                break;
            default:                    /* free stroke */
                gl_line(glyph[n], x, y, rnd() % 7, rnd() % 7);
                break;
            }
        }
    }
}

static void init_rain(void)
{
    int c;

    for (c = 0; c < RCOLS; c++) {
        r_speed[c] = (u8)(9 + rnd() % 18);              /* cells / second */
        r_trail[c] = (u8)(6 + rnd() % 14);
        r_period[c] = (u8)(25 + r_trail[c] + 8 + rnd() % 25);
        r_off[c] = (u8)(rnd() % r_period[c]);
        r_fade[c] = (31 * 256) / r_trail[c];
    }
}

static void blit_glyph(int x, int y, const u8 *g, u8 col)
{
    u8 *p = fb + y * SW + x;
    int r, b;

    STAT(st_glyphs++);
    for (r = 0; r < 8; r++) {
        b = g[r];
        if (b) {
            if (b & 0x80) p[0] = col;
            if (b & 0x40) p[1] = col;
            if (b & 0x20) p[2] = col;
            if (b & 0x10) p[3] = col;
            if (b & 0x08) p[4] = col;
            if (b & 0x04) p[5] = col;
            if (b & 0x02) p[6] = col;
            if (b & 0x01) p[7] = col;
        }
        p += SW;
    }
}

/* tc: time inside the 44 s cycle (rain pattern), ms: running time (flicker) */
static void draw_rain(u32 tc, u32 ms)
{
    int c, k, row, head, trail, gi, cp;
    u32 hp;

    for (c = 0; c < RCOLS; c++) {
        hp = (tc * (u32)(r_speed[c] * 256) / 1000u + (u32)r_off[c] * 256u)
             % ((u32)r_period[c] * 256u);
        head = (int)(hp >> 8);
        trail = r_trail[c];
        cp = (int)((ms + (u32)c * 37u) / 110u);
        for (k = 0; k < trail; k++) {
            row = head - k;
            if (row < 0 || row >= RROWS)
                continue;
            gi = (c * 131 + row * 71 + (cp + (row & 7)) * 29) & (NGLYPH - 1);
            blit_glyph(c * 8, row * 8, glyph[gi],
                       (u8)(k == 0 ? PAL_HEAD : PAL_RAIN + 31 - ((k * r_fade[c]) >> 8)));
        }
    }
}

/* ---- primitives ---- */

static void line(int x0, int y0, int x1, int y1, u8 col)
{
    int dx, dy, sx, sy, err, e2;

    if (x0 < 0 || x0 >= SW || x1 < 0 || x1 >= SW
        || y0 < 0 || y0 >= SH || y1 < 0 || y1 >= SH)
        return;
    dx = (x1 > x0) ? x1 - x0 : x0 - x1;
    dy = (y1 > y0) ? y0 - y1 : y1 - y0;
    sx = (x0 < x1) ? 1 : -1;
    sy = (y0 < y1) ? 1 : -1;
    err = dx + dy;
    for (;;) {
        fb[y0 * SW + x0] = col;
        if (x0 == x1 && y0 == y1)
            break;
        e2 = 2 * err;
        if (e2 >= dy) {
            err += dy;
            x0 += sx;
        }
        if (e2 <= dx) {
            err += dx;
            y0 += sy;
        }
    }
}

/* Gouraud triangle.  Colours are palette offsets in 8.8 fixed point (0..63.99). */
static void tri(int x0, int y0, int c0, int x1, int y1, int c1,
                int x2, int y2, int c2)
{
    int t, area, dcdx, dcdy, y, xl, xr, c, n, v, cmin, cmax;
    int da, db, dc, xa, xb, dy01, dy12, dy02;
    u8 *p;

    if (y1 < y0) { t = x0; x0 = x1; x1 = t; t = y0; y0 = y1; y1 = t; t = c0; c0 = c1; c1 = t; }
    if (y2 < y0) { t = x0; x0 = x2; x2 = t; t = y0; y0 = y2; y2 = t; t = c0; c0 = c2; c2 = t; }
    if (y2 < y1) { t = x1; x1 = x2; x2 = t; t = y1; y1 = y2; y2 = t; t = c1; c1 = c2; c2 = t; }

    cmin = c0;                          /* never leave the vertex colour range */
    cmax = c0;
    if (c1 < cmin) cmin = c1;
    if (c2 < cmin) cmin = c2;
    if (c1 > cmax) cmax = c1;
    if (c2 > cmax) cmax = c2;

    dy02 = y2 - y0;
    if (dy02 == 0)
        return;
    dy01 = y1 - y0;
    dy12 = y2 - y1;
    area = (x1 - x0) * dy02 - (x2 - x0) * dy01;
    if (area == 0)
        return;
    STAT(st_tris++);
    dcdx = ((c1 - c0) * dy02 - (c2 - c0) * dy01) / area;        /* colour plane */
    dcdy = ((c2 - c0) * (x1 - x0) - (c1 - c0) * (x2 - x0)) / area;
    if (dcdx > 20000) dcdx = 20000;
    if (dcdx < -20000) dcdx = -20000;
    if (dcdy > 20000) dcdy = 20000;
    if (dcdy < -20000) dcdy = -20000;

    da = ((x2 - x0) * 65536) / dy02;
    db = dy01 ? ((x1 - x0) * 65536) / dy01 : 0;
    dc = dy12 ? ((x2 - x1) * 65536) / dy12 : 0;
    xa = x0 * 65536;
    xb = xa;

    for (y = y0; y < y2; y++) {
        if (y == y1)
            xb = x1 * 65536;
        xl = xa >> 16;
        xr = xb >> 16;
        if (xl > xr) {
            t = xl; xl = xr; xr = t;
        }
        if (y >= 0 && y < SH) {
            c = c0 + dcdx * (xl - x0) + dcdy * (y - y0);
            if (xl < 0) {
                c += dcdx * (0 - xl);
                xl = 0;
            }
            if (xr > SW - 1)
                xr = SW - 1;
            p = fb + y * SW + xl;
            for (n = xr - xl; n >= 0; n--) {
                v = c;
                if (v < cmin)
                    v = cmin;
                else if (v > cmax)
                    v = cmax;
                *p++ = (u8)(PAL_MESH + (v >> 8));
                c += dcdx;
            }
            STAT(st_pix += (xr - xl + 1));
        }
        xa += da;
        xb += (y < y1) ? db : dc;
    }
}

/* ---- the cube / ball: a cube whose faces are 6x6 quads, morphing to a sphere ---- */

#define MN   6                          /* quads per face edge */
#define MV   ((MN + 1) * (MN + 1))      /* vertices per face */
#define NV   (6 * MV)
#define PERSP_F   186
#define PERSP_Z   1600
#define SPH_R     300
#define LX (-6000)                      /* light direction, 14-bit */
#define LY (-6000)
#define LZ (-13900)

static const signed char fn_[6][3] = {
    { 1, 0, 0 }, { -1, 0, 0 }, { 0, 1, 0 }, { 0, -1, 0 }, { 0, 0, 1 }, { 0, 0, -1 }
};
static const signed char fu_[6][3] = {
    { 0, 1, 0 }, { 0, 0, 1 }, { 0, 0, 1 }, { 1, 0, 0 }, { 1, 0, 0 }, { 0, 1, 0 }
};
static const signed char fv_[6][3] = {
    { 0, 0, 1 }, { 0, 1, 0 }, { 1, 0, 0 }, { 0, 0, 1 }, { 0, 1, 0 }, { 1, 0, 0 }
};

static short vcube[NV][3], vsph[NV][3], nsph[NV][3];
static u8 vgrad[NV];
static short vsx[NV], vsy[NV], vzz[NV];
static int vcol[NV];

static void init_mesh(void)
{
    int f, i, j, k, v, u, w, len, g;
    int p[3];

    for (f = 0; f < 6; f++)
        for (i = 0; i <= MN; i++)
            for (j = 0; j <= MN; j++) {
                v = f * MV + i * (MN + 1) + j;
                u = -256 + (512 * i) / MN;
                w = -256 + (512 * j) / MN;
                for (k = 0; k < 3; k++)
                    p[k] = fn_[f][k] * 256 + fu_[f][k] * u + fv_[f][k] * w;
                len = isqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
                g = 31 + ((p[0] + p[1] + p[2]) * 315) / 6144;     /* wide gradient */
                vgrad[v] = (u8)(g < 0 ? 0 : (g > 63 ? 63 : g));
                for (k = 0; k < 3; k++) {
                    vcube[v][k] = (short)p[k];
                    vsph[v][k] = (short)((p[k] * SPH_R) / len);
                    nsph[v][k] = (short)((p[k] * 16384) / len);
                }
            }
}

static void mat_mul(int a[3][3], int b[3][3], int r[3][3])
{
    int i, j;

    for (i = 0; i < 3; i++)
        for (j = 0; j < 3; j++)
            r[i][j] = (a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j]) >> 14;
}

static void draw_mesh(int ocx, int ocy, int scale, u32 ms, int m)
{
    int rx[3][3], ry[3][3], rz[3][3], t1[3][3], rot[3][3], sm[3][3];
    int lobj[3], dotf[6], fsh[6];
    int ax, ay, az, sn, cs, f, i, j, v, a, b, c, d, area, x, y, z, inv;
    int px, py, pz, dots, light, col, za, lv, ymin = SH, ymax = -1;

    ax = (int)((ms * 174u / 1000u) & 1023u);
    ay = (int)((ms * 266u / 1000u) & 1023u);
    az = (int)((ms * 113u / 1000u) & 1023u);

    sn = isin(ax); cs = icos(ax);
    rx[0][0] = 16384; rx[0][1] = 0;   rx[0][2] = 0;
    rx[1][0] = 0;     rx[1][1] = cs;  rx[1][2] = -sn;
    rx[2][0] = 0;     rx[2][1] = sn;  rx[2][2] = cs;
    sn = isin(ay); cs = icos(ay);
    ry[0][0] = cs;    ry[0][1] = 0;   ry[0][2] = sn;
    ry[1][0] = 0;     ry[1][1] = 16384; ry[1][2] = 0;
    ry[2][0] = -sn;   ry[2][1] = 0;   ry[2][2] = cs;
    sn = isin(az); cs = icos(az);
    rz[0][0] = cs;    rz[0][1] = -sn; rz[0][2] = 0;
    rz[1][0] = sn;    rz[1][1] = cs;  rz[1][2] = 0;
    rz[2][0] = 0;     rz[2][1] = 0;   rz[2][2] = 16384;
    mat_mul(ry, rx, t1);
    mat_mul(rz, t1, rot);
    for (i = 0; i < 3; i++)
        for (j = 0; j < 3; j++)
            sm[i][j] = (rot[i][j] * scale) >> 8;

    /* light direction in object space = rot^T * L */
    for (j = 0; j < 3; j++)
        lobj[j] = (rot[0][j] * LX + rot[1][j] * LY + rot[2][j] * LZ) >> 14;
    for (f = 0; f < 6; f++) {
        dotf[f] = fn_[f][0] * lobj[0] + fn_[f][1] * lobj[1] + fn_[f][2] * lobj[2];
        fsh[f] = (dotf[f] * 3584) >> 14;        /* +-14 palette steps, 8.8 */
    }

    for (f = 0; f < 6; f++)
        for (i = 0; i < MV; i++) {
            v = f * MV + i;
            if (m == 0) {
                px = vcube[v][0]; py = vcube[v][1]; pz = vcube[v][2];
            } else if (m == 256) {
                px = vsph[v][0]; py = vsph[v][1]; pz = vsph[v][2];
            } else {
                px = vcube[v][0] + (((vsph[v][0] - vcube[v][0]) * m) >> 8);
                py = vcube[v][1] + (((vsph[v][1] - vcube[v][1]) * m) >> 8);
                pz = vcube[v][2] + (((vsph[v][2] - vcube[v][2]) * m) >> 8);
            }
            x = (sm[0][0] * px + sm[0][1] * py + sm[0][2] * pz) >> 14;
            y = (sm[1][0] * px + sm[1][1] * py + sm[1][2] * pz) >> 14;
            z = (sm[2][0] * px + sm[2][1] * py + sm[2][2] * pz) >> 14;
            inv = (PERSP_F * 65536) / (z + PERSP_Z);
            vsx[v] = (short)(ocx + ((x * inv) >> 16));
            vsy[v] = (short)(ocy + ((((y * inv) >> 16) * 213) >> 8));  /* 5:6 pixels */
            vzz[v] = (short)z;
            if (vsy[v] < ymin)
                ymin = vsy[v];
            if (vsy[v] > ymax)
                ymax = vsy[v];

            col = vgrad[v] * 256 + fsh[f];              /* cube: gradient + shading */
            if (m > 0) {                                /* ball: lit green */
                dots = (nsph[v][0] * lobj[0] + nsph[v][1] * lobj[1]
                        + nsph[v][2] * lobj[2]) >> 14;
                if (dots < 0)
                    dots = 0;
                light = 6 * 256 + ((dots * (57 * 256)) >> 14);
                col = ((256 - m) * col + m * light) >> 8;
            }
            if (col < 0)
                col = 0;
            else if (col > 63 * 256)
                col = 63 * 256;
            vcol[v] = col;
        }

    ymin -= 2;                          /* remember the rows we are about to touch */
    ymax += 2;
    if (ymin < 0)
        ymin = 0;
    if (ymax > BAR_TOP - 1)
        ymax = BAR_TOP - 1;
    if (ymin <= ymax) {
        if (ymin < cur_lo)
            cur_lo = ymin;
        if (ymax > cur_hi)
            cur_hi = ymax;
    }

    for (f = 0; f < 6; f++)
        for (i = 0; i < MN; i++)
            for (j = 0; j < MN; j++) {
                a = f * MV + i * (MN + 1) + j;
                b = a + MN + 1;
                c = b + 1;
                d = a + 1;
                area = (vsx[b] - vsx[a]) * (vsy[c] - vsy[a])
                     - (vsx[c] - vsx[a]) * (vsy[b] - vsy[a]);
                if (area >= 0)                          /* back face */
                    continue;
                tri(vsx[a], vsy[a], vcol[a], vsx[b], vsy[b], vcol[b],
                    vsx[c], vsy[c], vcol[c]);
                tri(vsx[a], vsy[a], vcol[a], vsx[c], vsy[c], vcol[c],
                    vsx[d], vsy[d], vcol[d]);
            }

    if (m < LINE_MORPH)
        return;
    for (f = 0; f < 6; f++)                             /* glowing wire-frame */
        for (i = 0; i < MN; i++)
            for (j = 0; j < MN; j++) {
                a = f * MV + i * (MN + 1) + j;
                b = a + MN + 1;
                c = b + 1;
                d = a + 1;
                area = (vsx[b] - vsx[a]) * (vsy[c] - vsy[a])
                     - (vsx[c] - vsx[a]) * (vsy[b] - vsy[a]);
                if (area >= 0)
                    continue;
                za = (vzz[a] + vzz[b]) >> 1;
                lv = 8 - ((za * 27) >> 10);
                line(vsx[a], vsy[a], vsx[b], vsy[b], (u8)(PAL_LINE + (lv < 0 ? 0 : lv > 15 ? 15 : lv)));
                za = (vzz[a] + vzz[d]) >> 1;
                lv = 8 - ((za * 27) >> 10);
                line(vsx[a], vsy[a], vsx[d], vsy[d], (u8)(PAL_LINE + (lv < 0 ? 0 : lv > 15 ? 15 : lv)));
                if (j == MN - 1) {
                    za = (vzz[d] + vzz[c]) >> 1;
                    lv = 8 - ((za * 27) >> 10);
                    line(vsx[d], vsy[d], vsx[c], vsy[c], (u8)(PAL_LINE + (lv < 0 ? 0 : lv > 15 ? 15 : lv)));
                }
                if (i == MN - 1) {
                    za = (vzz[b] + vzz[c]) >> 1;
                    lv = 8 - ((za * 27) >> 10);
                    line(vsx[b], vsy[b], vsx[c], vsy[c], (u8)(PAL_LINE + (lv < 0 ? 0 : lv > 15 ? 15 : lv)));
                }
            }
}

/* ---- terminal-style scroller (right to left) ---- */

static const char scroll_text[] = "Book386 Linux demo by astr0claude 2026, greetings to Reality Kiwis from the future. ";

#define NFONT   91                      /* ASCII 32..122 */
#define TXT_PX  (((int)sizeof(scroll_text) - 1) * 6 * TXT_SCALE)
#define STRIP_W (TXT_PX + 2 * SW)       /* blank screen + text + blank screen */

static const u8 font5x7[NFONT][8] = {
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* space */
    { 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00 }, /* '!' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '"' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '#' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '$' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '%' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '&' */
    { 0x04, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '\'' */
    { 0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02, 0x00 }, /* '(' */
    { 0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08, 0x00 }, /* ')' */
    { 0x00, 0x15, 0x0E, 0x1F, 0x0E, 0x15, 0x00, 0x00 }, /* '*' */
    { 0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00, 0x00 }, /* '+' */
    { 0x00, 0x00, 0x00, 0x00, 0x0C, 0x04, 0x08, 0x00 }, /* ',' */
    { 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00 }, /* '-' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00 }, /* '.' */
    { 0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10, 0x00 }, /* '/' */
    { 0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E, 0x00 }, /* '0' */
    { 0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E, 0x00 }, /* '1' */
    { 0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F, 0x00 }, /* '2' */
    { 0x1F, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0E, 0x00 }, /* '3' */
    { 0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02, 0x00 }, /* '4' */
    { 0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E, 0x00 }, /* '5' */
    { 0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E, 0x00 }, /* '6' */
    { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00 }, /* '7' */
    { 0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E, 0x00 }, /* '8' */
    { 0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C, 0x00 }, /* '9' */
    { 0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x00, 0x00 }, /* ':' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* ';' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '<' */
    { 0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00, 0x00 }, /* '=' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '>' */
    { 0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04, 0x00 }, /* '?' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '@' */
    { 0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00 }, /* 'A' */
    { 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E, 0x00 }, /* 'B' */
    { 0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E, 0x00 }, /* 'C' */
    { 0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E, 0x00 }, /* 'D' */
    { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F, 0x00 }, /* 'E' */
    { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10, 0x00 }, /* 'F' */
    { 0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E, 0x00 }, /* 'G' */
    { 0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00 }, /* 'H' */
    { 0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E, 0x00 }, /* 'I' */
    { 0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C, 0x00 }, /* 'J' */
    { 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00 }, /* 'K' */
    { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F, 0x00 }, /* 'L' */
    { 0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11, 0x00 }, /* 'M' */
    { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11, 0x00 }, /* 'N' */
    { 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00 }, /* 'O' */
    { 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10, 0x00 }, /* 'P' */
    { 0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D, 0x00 }, /* 'Q' */
    { 0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11, 0x00 }, /* 'R' */
    { 0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E, 0x00 }, /* 'S' */
    { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00 }, /* 'T' */
    { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00 }, /* 'U' */
    { 0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00 }, /* 'V' */
    { 0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11, 0x00 }, /* 'W' */
    { 0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11, 0x00 }, /* 'X' */
    { 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x00 }, /* 'Y' */
    { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F, 0x00 }, /* 'Z' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '[' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '\\' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* ']' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '^' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00 }, /* '_' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '`' */
    { 0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F, 0x00 }, /* 'a' */
    { 0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x1E, 0x00 }, /* 'b' */
    { 0x00, 0x00, 0x0E, 0x11, 0x10, 0x11, 0x0E, 0x00 }, /* 'c' */
    { 0x01, 0x01, 0x0F, 0x11, 0x11, 0x11, 0x0F, 0x00 }, /* 'd' */
    { 0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E, 0x00 }, /* 'e' */
    { 0x06, 0x09, 0x08, 0x1C, 0x08, 0x08, 0x08, 0x00 }, /* 'f' */
    { 0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x11, 0x0E }, /* 'g' */
    { 0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00 }, /* 'h' */
    { 0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E, 0x00 }, /* 'i' */
    { 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x12, 0x0C }, /* 'j' */
    { 0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12, 0x00 }, /* 'k' */
    { 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E, 0x00 }, /* 'l' */
    { 0x00, 0x00, 0x1A, 0x15, 0x15, 0x11, 0x11, 0x00 }, /* 'm' */
    { 0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00 }, /* 'n' */
    { 0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E, 0x00 }, /* 'o' */
    { 0x00, 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10 }, /* 'p' */
    { 0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x01 }, /* 'q' */
    { 0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10, 0x00 }, /* 'r' */
    { 0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E, 0x00 }, /* 's' */
    { 0x08, 0x08, 0x1C, 0x08, 0x08, 0x09, 0x06, 0x00 }, /* 't' */
    { 0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D, 0x00 }, /* 'u' */
    { 0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00 }, /* 'v' */
    { 0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A, 0x00 }, /* 'w' */
    { 0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x00 }, /* 'x' */
    { 0x00, 0x11, 0x11, 0x11, 0x0F, 0x01, 0x11, 0x0E }, /* 'y' */
    { 0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F, 0x00 }, /* 'z' */
};

static u8 strip[STRIP_H * STRIP_W];     /* the whole message, pre-rendered */

/* Render the message once: bright green text with a dark green glow. */
static void init_scroller(void)
{
    int i, ch, row, col, x0, pass, dx, dy, ox, oy;
    u8 *p;

    memset(strip, 0, sizeof(strip));
    for (pass = 0; pass < 2; pass++) {          /* glow first, text on top */
        ox = oy = (pass == 0) ? 2 : 0;
        for (i = 0; scroll_text[i]; i++) {
            ch = scroll_text[i] - 32;
            if (ch < 0 || ch >= NFONT)
                continue;
            x0 = SW + i * 6 * TXT_SCALE;
            for (row = 0; row < 8; row++)
                for (col = 0; col < 5; col++) {
                    if (!(font5x7[ch][row] & (0x10 >> col)))
                        continue;
                    for (dy = 0; dy < TXT_SCALE; dy++) {
                        p = strip + (row * TXT_SCALE + dy + oy) * STRIP_W
                            + x0 + col * TXT_SCALE + ox;
                        for (dx = 0; dx < TXT_SCALE; dx++)
                            *p++ = (u8)(PAL_TEXT + 1 - pass);
                    }
                }
        }
    }
}

static void draw_bar(u32 ms)
{
    int off, r;

    memset(fb + BAR_TOP * SW, 0, (SH - BAR_TOP) * SW);
    memset(fb + BAR_TOP * SW, PAL_TEXT + 2, SW);        /* thin line on top */
    off = (int)((ms * SCROLL_PXS / 1000u) % (u32)(STRIP_W - SW + 1));
    for (r = 0; r < STRIP_H; r++)
        memcpy(fb + (TXT_Y + r) * SW, strip + r * STRIP_W + off, SW);
}

/* ---- the show: everything is a function of time ---- */

#define T_CYCLE 44000                   /* ms: cube -> bounce -> ball+rain -> cube */
#define CX0   160                   /* centre of the play area */
#define CY0   86
#define BX_LO 78
#define BX_HI 242
#define BY_LO 64                    /* the bounce keeps the cube clear of the bar */
#define BY_HI 108

static int fold(int p, int range)       /* triangle wave over 0..range */
{
    p %= 2 * range;
    if (p > range)
        p = 2 * range - p;
    return p;
}

static void render_frame(u32 ms)
{
    int tc, cyc, morph, rain, wb, bx, by, ocx, ocy, scale, dt, sway, bob;

    cyc = (int)(ms / T_CYCLE);
    tc = (int)(ms % T_CYCLE);

    if (tc < 16000)                     /* cube spins, then bounces */
        morph = 0;
    else if (tc < 20500)                /* cube -> cyber ball */
        morph = smooth(((tc - 16000) * 256) / 4500);
    else if (tc < 40000)
        morph = 256;
    else                                /* ball -> cube */
        morph = 256 - smooth(((tc - 40000) * 256) / 4000);

    if (tc < 17000)
        rain = 0;
    else if (tc < 21000)
        rain = smooth(((tc - 17000) * 256) / 4000);
    else if (tc < 39500)
        rain = 256;
    else if (tc < 43500)
        rain = 256 - smooth(((tc - 39500) * 256) / 4000);
    else
        rain = 0;

    bx = CX0;
    by = CY0;
    if (tc >= 6000) {                   /* bounce around the screen */
        dt = tc - 6000;
        bx = BX_LO + fold((CX0 - BX_LO) * 1000 + 62 * dt, (BX_HI - BX_LO) * 1000) / 1000;
        by = BY_LO + fold((CY0 - BY_LO) * 1000 + 41 * dt, (BY_HI - BY_LO) * 1000) / 1000;
    }
    if (tc < 16000)
        wb = 256;
    else if (tc < 20500)                /* glide back to the middle while morphing */
        wb = 256 - smooth(((tc - 16000) * 256) / 4500);
    else
        wb = 0;
    ocx = CX0 + (((bx - CX0) * wb) >> 8);
    ocy = CY0 + (((by - CY0) * wb) >> 8);

    sway = (isin((int)(ms * 2u / 10u)) * 5) >> 14;      /* ball floats gently */
    bob = (isin((int)(ms * 3u / 10u)) * 7) >> 14;
    ocx += (sway * morph) >> 8;
    ocy += (bob * morph) >> 8;

    scale = 256;
    if (cyc == 0 && ms < 1800)          /* zoom in at the very start */
        scale = 60 + ((smooth((int)(ms * 256u / 1800u)) * 196) >> 8);

    want_morph = morph;
    want_rain = rain;

    memset(fb + prev_lo * SW, 0, (prev_hi - prev_lo + 1) * SW);  /* erase last frame */
    cur_lo = SH;
    cur_hi = -1;
    if (rain > 0) {
        draw_rain((u32)tc, ms);
        cur_lo = 0;
        cur_hi = BAR_TOP - 1;
    }
    draw_mesh(ocx, ocy, scale, ms, morph);
    draw_bar(ms);                       /* scroller over whatever reached the bottom */
}

static void present(void)
{
    int lo = (prev_lo < cur_lo) ? prev_lo : cur_lo;
    int hi = (prev_hi > cur_hi) ? prev_hi : cur_hi;

    if (hi >= lo)                       /* rows of this and of the last frame */
        memcpy(vgamem + lo * SW, fb + lo * SW, (hi - lo + 1) * SW);
    memcpy(vgamem + BAR_TOP * SW, fb + BAR_TOP * SW, (SH - BAR_TOP) * SW);   /* the bar */
    prev_lo = cur_lo;
    prev_hi = cur_hi;
    if (prev_hi < prev_lo) {            /* nothing was drawn */
        prev_lo = 0;
        prev_hi = -1;
    }
}

/* ================================================================== */
/*  main                                                               */
/* ================================================================== */

static struct timeval t_start;

static u32 now_ms(void)
{
    struct timeval tv;

    gettimeofday(&tv, NULL);
    return (u32)((tv.tv_sec - t_start.tv_sec) * 1000
                 + (tv.tv_usec - t_start.tv_usec) / 1000);
}

static void on_signal(int sig)
{
    (void)sig;
    quit = 1;
}

static void usage(void)
{
    fputs("CYBER CUBE - 386SX demo (RAD v1.0 music + svgalib)\n"
          "usage: cybercube [-p port] [-v] [-f] [-m] [-h]\n"
          "  -p port  OPL base port in hex (default 388)\n"
          "  -v       wait for vertical retrace (no tearing, lower fps)\n"
          "  -f       skip OPL chip detection\n"
          "  -m       mute: no music, no OPL access\n"
          "ESC or q quits.  Needs root.\n", stderr);
    exit(1);
}

int main(int argc, char **argv)
{
    extern char *optarg;
    int c, key, mute = 0, nodetect = 0, vsync = 0;
    u32 frames = 0, ms, total;
    const char *err;

    while ((c = getopt(argc, argv, "p:vfmh")) != -1) {
        switch (c) {
        case 'p': opl_port = (int)strtol(optarg, NULL, 16); break;
        case 'v': vsync = 1; break;
        case 'f': nodetect = 1; break;
        case 'm': mute = 1; break;
        default:  usage();
        }
    }

    err = rad_validate();
    if (err) {
        fprintf(stderr, "track.h: %s\n", err);
        return 1;
    }
    if (!mute) {                        /* before svgalib drops root */
        if (ioperm(opl_port, 4, 1) != 0) {
            perror("ioperm (are you root?)");
            return 1;
        }
        if (!nodetect && !opl_detect()) {
            fprintf(stderr, "no OPL chip found at port 0x%X (-f forces, -m mutes)\n",
                    opl_port);
            return 1;
        }
        opl_reset();
    }

    init_palettes();
    init_glyphs();
    init_rain();
    init_mesh();
    init_scroller();

    vga_init();
    if (!vga_hasmode(G320x200x256)) {
        fprintf(stderr, "320x200x256 is not available\n");
        return 1;
    }
    vga_setmode(G320x200x256);
    vgamem = (u8 *)vga_getgraphmem();
    vga_setpalette(0, 0, 0, 0);
    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    if (!mute) {
        rad_start();
        timer_start();
    }

    gettimeofday(&t_start, NULL);
    while (!quit) {
        ms = now_ms();
        render_frame(ms);
        if (vsync)
            vga_waitretrace();
        present();
        apply_palettes();
        frames++;
        key = vga_getkey();
        if (key == 27 || key == 'q' || key == 'Q')
            break;
    }
    total = now_ms();

    timer_stop();
    if (!mute) {
        opl_reset();                    /* silence */
        ioperm(opl_port, 4, 0);
    }
    vga_setmode(TEXT);
    if (total > 0)
        printf("%u frames in %u.%03u s = %u.%u fps\n", frames, total / 1000,
               total % 1000, (frames * 1000u) / total,
               ((frames * 10000u) / total) % 10);
    return 0;
}
