// The Xmas Demo 2024 Control Code
// Authors: Julin and Corneliusen
// "We do this not because it is easy, but because we thought it would be easy."
// Runs in 60 fps on NVidia Jetson Nano. Max out the GPU clock. Have a proper power supply. Duh.
// More info here: https://www.ignorantus.com/xmasdemo/
// License: https://creativecommons.org/publicdomain/zero/1.0/

// This Xmas Demo uses the V73D engine by Sjur Julin.
// More info here: https://github.com/sjulin/V73D

#include <V73D/V7.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>

#ifdef _WIN32
#include <Windows.h>
#elif __linux__
#include <unistd.h>
#endif

#include <V73D/font/sdf_babylon5.h>
#include <V73D/font/sdf_unispace.h>

#include "vec.h"

// Set by options
static bool shdebug = false;
static bool ctrldebug = false;
static bool savepics = false;
static bool gentitle = false;
static int pauseframe = -1;
static int startpart = 0;

static int shaderw, shaderh;

static double demoduration;
static int demoframes;
static int addframe = 1;

static int framectr;
static int totalframes;

#ifdef _WIN32
#define SAVEPATH "..\\..\\tmp\\out%05d.jpg"
#else
#define SAVEPATH "../render/out%05d.jpg"
#endif

#define TEXX(mod) ((mod)->mtr->coltex)

static V7X *v7x;

static Model *lines;
static Model *txt_scroll, *txt_scrolls, *txt_top, *txt_tops, *txt_quirk, *txt_credl, *txt_credr, *txt_debug1, *txt_debug1s, *txt_debug2, *txt_debug2s;
static Model *pic_bike1, *pic_bike2, *pic_bike3, *pic_bike4, *pic_moi, *pic_pig;
static Model *swintrek;
static Anim *swintrek_anim;

typedef struct {
    float in_factor;
    float antic, pokey, ctia;
} GPUGeneric;
static GPUGeneric gpugen;

#define THINGCNT 18
typedef struct {
    v4 things[THINGCNT];
    float smallsize;
    float wingus, dingus, zapp;
} GPUGalDance;
static GPUGalDance gpugaldance;

typedef struct {
    v4 in_p;
    v4 in_cx, in_cy, in_cz;
    float in_factor;
    float sega, bally, midway;
} GPUAcid;
static GPUAcid gpuacid;

typedef struct {
    v4 in_scr;
    v4 in_u;
    v4 in_v;
    v4 in_prp;
    float jtime;
    float agnus, paula, denise;
} GPUJourney;
static GPUJourney gpujourney;

typedef struct {
    v4 mu_pos;
    float rot_xz;
    float zoom;
    float tom, jerry;
} GPUQuat;
static GPUQuat gpuquat;

#define TABLELEN 512
#define TABLEMASK (TABLELEN-1)
typedef struct {
    float in_factor;
    float akiko, gayle, bridgette;
    v4 noisetable[TABLELEN];
} GPUTunnel;
static GPUTunnel gputunnel;

#define TFNUM     5
#define LIGHTNUM 12
typedef struct {
    v4 lightpos[LIGHTNUM];
    v4 lightcol[LIGHTNUM];
    v4 b1[TFNUM];
    v4 b2[TFNUM];
    v4 eye;
    float scale;
    float in_factor;
    float pinky, clyde;
} GPUTwofield;
static GPUTwofield gputwofield;

#define ROTS  5
#define CORS 24
typedef struct {
    v4 rots[ROTS];
    v4 cors[CORS];
    float in_factor;
    float burwor, garwor, thorwor;
} GPUKnots;
static GPUKnots gpuknots;

typedef enum {
    PART_WHITENED,
    PART_INTRO,
    PART_SPONGE,
    PART_JOURNEY,
    PART_ACID,
    PART_QUAT,
    PART_TUNNEL,
    PART_TWOFIELD,
    PART_GLASSY,
    PART_KNOTS,
    PART_KALEIDO,
    PART_OUTRO,
    PART_BLACKENED,
    BODYPARTS
} PartId;

typedef struct {
    PartId pid;
    char *fname;
    char *title;
    void *data;
    int datalen;
    double starttime;
    int width, height;
    char *scrolltext;
    char *toptext;
    char *credtext;
    double duration; // calculated by genduration()
} DemoPart;

#define SPC_GALA "                                                                        "
#define SPC_ACID "                                               "
#define SPC_JOUR "                                 /                                 "
#define SPC_SPON "                                                                        "
#define SPC_QUAT "                                                                                    "
#define SPC_TUNN "                                                                                              "
#define SPC_TWOF "                                                        "
#define SPC_GLAS "                       /                       "
#define SPC_KNOT "                                                          "
#define SPC_KALE "                                                                         "
#define TXT_INTRO    "                     Corneliusen & Julin Productions" SPC_GALA "                NVidia Jetson Nano"
#define TXT_SPONGE   "Hardware cost: $99" SPC_ACID "The size of a matchbox" SPC_ACID "128 GPU cores @ 921 MHz" SPC_ACID "Do more with less!" SPC_ACID "60 fps always. Take no prisoners!"
#define TXT_JOURNEY  "And all the cores jam up with snake shit" SPC_JOUR "And there's nothing you can do" SPC_JOUR "It's all just bits of energy" SPC_JOUR "Flying away from you"
#define TXT_ACID     "Music: \"Against the Clock\" by Chris Huelsbeck" SPC_SPON "Video recorded live @ 59.912944,10.754639 December 17th, 2024."
#define TXT_QUAT     "Full credits, source code & artwork" SPC_QUAT "ignorantus.com/xmasdemo"
#define TXT_TUNNEL   "And now, another controvertible episode of... Pigs in Space!" SPC_TUNN "Is this the end of Pigs in Space?"
#define TXT_TWOFIELD "NVidia Jetson Nano" SPC_TWOF "Like a third of a TX2 from 2017" SPC_TWOF "60 fps always. Never surrender!"
#define TXT_GLASSY   "Hey now, all you coders" SPC_GLAS "Leave your lights on" SPC_GLAS "Better leave your lights on" SPC_GLAS "'Cause there's a python livin' in your PC" SPC_GLAS "Whisperin' in your ear"
#define TXT_KNOTS    "C: 52 years old and thriving." SPC_KNOT "C will be around for another 52 years." SPC_KNOT "Where will python, unstable rust, floop etc. end up?" SPC_KNOT "Signs point to Alamogordo, New Mexico."
#define TXT_KALEIDO  "Greetings to" SPC_KALE "Scoopex, No Limits, New Wave, Triumph, Cryptoburners, Spaceballs, Offence, Crusaders, Cinefex Design, and the rest!" SPC_KALE "Save the planet! Write code in C!"
#define TXT_OUTRO    "                onaN nosteJ aidiVN" SPC_GALA "                     snoitcudorP niluJ & nesuilenroC"

static DemoPart demo_parts[] = {
    { PART_WHITENED,  "noisy.fs",    "Whitened",        &gpugen,      sizeof(gpugen),        0.0, 1920,960, "",           "",                            "\nBased on \"VHS Tape Noise\" by vladstorm" },
    { PART_INTRO,     "galdance.fs", "Intro",           &gpugaldance, sizeof(gpugaldance),   5.0, 1920,960, TXT_INTRO,    "\x02""The Xmas Demo 2024\n",  "\nBased on \"Galactic Dance\" by Sinuousity" },
    { PART_SPONGE,    "sponge.fs",   "Spongified",      &gpugen,      sizeof(gpugen),       19.0,  960,480, TXT_SPONGE,   "\x02""NVidia Jetson Nano\n",  "\nBased on \"Menger Sponge Tunnel\" by kithy" },
    { PART_JOURNEY,   "journey.fs",  "Journeyman",      &gpujourney,  sizeof(gpujourney),   42.0,  640,320, TXT_JOURNEY,  "\x02""The $99 Xmas Demo\n",   "\nBased on \"Torus Journey\" by PauloFalcao" },
    { PART_ACID,      "acid.fs",     "Acid Houses",     &gpuacid,     sizeof(gpuacid),      65.0,  800,400, TXT_ACID,     "\x02""Dove Nation Ninjas\n",  "\nBased on \"Paper City\" by NuSan" },
    { PART_QUAT,      "quatio.fs",   "Quatio",          &gpuquat,     sizeof(gpuquat),      80.5,  800,400, TXT_QUAT,     "\x02""Xmas Demo VI\n",        "\nBased on recycled crap by Corneliusen" },
    { PART_TUNNEL,    "tunnel.fs",   "Pigs in Tunnel",  &gputunnel,   sizeof(gputunnel),    96.0,  800,400, TXT_TUNNEL,   "",                            "\nBased on \"Colorful Smoke Tunnel\" by meowyih" },
    { PART_TWOFIELD,  "twofield.fs", "Twofield System", &gputwofield, sizeof(gputwofield), 111.5,  960,480, TXT_TWOFIELD, "\x02""Do Ninjas Innovate?\n", "\nBased on \"Twofield\" by w23" },
    { PART_GLASSY,    "glassy.fs",   "Glassy Knoll",    &gpugen,      sizeof(gpugen),      126.5,  640,320, TXT_GLASSY,   "\x02""The Xmas Demo Lives\n", "\nBased on \"Transparent Lattice\" by Shane" },
    { PART_KNOTS,     "knots.fs",    "Knots Landing",   &gpuknots,    sizeof(gpuknots),    149.5,  960,480, TXT_KNOTS,    "\x02""Deviant Ninja Soon\n",  "\nBased on \"Acrilic Knots\" by Elsio" },
    { PART_KALEIDO,   "kaleido.fs",  "Kaleidonope",     &gpugen,      sizeof(gpugen),      173.0, 1920,960, TXT_KALEIDO,  "\x02""Corneliusen & Julin\n", "\nBased on \"Simple Kishimisu\" by omansounds" },
    { PART_OUTRO,     "galdance.fs", "Outro",           &gpugaldance, sizeof(gpugaldance), 196.0, 1920,960, TXT_OUTRO,    "\x02""4202 omeD samX ehT\n",  "\nytisuouniS yb \"ecnaD citcalaG\" no desaB" },
    { PART_BLACKENED, "noisy.fs",    "Blackened",       &gpugen,      sizeof(gpugen),      212.0, 1920,960, "",           "",                            "\nBased on \"VHS Tape Noise\" by vladstorm" },
    { BODYPARTS,      NULL,          NULL,              NULL,         0,                   217.0,    0,  0, NULL,         NULL,                          NULL }, // last entry endtime only
};

#define DEMOPARTS ((int)(sizeof(demo_parts)/sizeof(demo_parts[0]))-1)

static Model *demo_models[DEMOPARTS];
static Model *bgmod;
static Texture *fbtex;

static Model *NewShader(char *name, char *shdname, void *gpudata, size_t gpusize) {
    printf( "NewShader: name=%s, fname=%s\n", name, shdname );
    Model *m = V7NewMod(v7x, name);
    char *shdsrc = V7Load(0, true, "../shaders/%s", shdname);
    if( shdebug ) {
        char *ptr = strstr( shdsrc, "//#define SHDEBUG" );
        if( ptr ) {
            printf( "Shader debug: %s\n", shdname );
            ptr[0] = ' ';
            ptr[1] = ' ';
        }
    }
    m->prg = V7CustPrg(v7x, name, shdsrc, shdname);
    if(gpudata) V7UniBuf(m, gpudata, gpusize);
    m->flags|=V7NMSK;
    return m;
}

Model *LoadMod(char *name) {
    printf( "LoadMod: name=%s\n", name );
    Model *m = V7GetMod(v7x, name);
    if(m) return m;
    char fn[64];
    snprintf(fn, sizeof(fn), "../data/%s.glb", name);
    size_t glb_size;
    void *glb = V7Load(&glb_size, 0, fn);
    if(!glb) {
        V7Error("Can't load model '%s'", fn);
        return 0;
    }
    m = V7GLB(v7x, name, glb, glb_size);
    m->Update = 0;
    return m;
}

static Model *LoadPicAsMod( Scene *s, char *fname, char *name, vec3 sc, vec3 t )
{
    printf( "LoadPicAsMod: name=%s, fname=%s\n", name, fname );
    Texture *tex;
    size_t piclen;
    void *pic = V7Load( &piclen, true, fname );

    char sname[80];
    snprintf( sname, sizeof(sname), "%sTex", name );
    tex = V7NewTex(v7x, sname);
    snprintf( sname, sizeof(sname), "%sImg", name );
    tex->img = V7NewImg(v7x, sname);
    size_t len = strlen(fname);
    if( len >= 4 && !strcmp( fname+len-4, ".jpg" ) )
        V7JPGDec(tex->img, pic, piclen);
    else
        V7PNGDec(tex->img, pic);
    V7ImgTex(tex);

    tex->mag = GL_LINEAR;
    glTextureParameteri( tex->id, GL_TEXTURE_MAG_FILTER, tex->mag);

    snprintf( sname, sizeof(sname), "%sMod", name );
    Model *mod = V7NewMod(v7x, sname);
    mod->prg = V7GetPrg(v7x, "RGB");
    V7Quad(mod->msh, 2.0f*(tex->img->w/(float)tex->img->h), 2.0f);
    snprintf( sname, sizeof(sname), "%sMtr", name );
    mod->mtr = V7NewMtr(v7x, sname);
    mod->mtr->coltex = tex;
    mod->s = sc;
    mod->t = t;
    V7SET(mod->flags, V7NDEP|V7NMSK);
    V7AddMod(s, mod);

    return mod;
}

static Model *NewText( char *title, SDFont *fn, vec3 sc, vec3 t, vec4 col )
{
    printf( "NewText: title=%s\n", title );
    Model *mo = V7NewTxt( v7x, title, fn );
    mo->s = sc;
    mo->t = t;
    Material oldmo = *mo->mtr;
    char mtrname[80];
    snprintf( mtrname, sizeof(mtrname), "%s-mtr", title );
    mo->mtr = V7NewMtr( v7x, mtrname );
    *mo->mtr = oldmo;
    mo->mtr->col = col;
    V7AddMod( v7x->scn, mo );
    return mo;
}

static void InvertTex( Texture *tex )
{
    for( int y = 0; y < (int)tex->img->h; y++ ) {
        uint32_t *row = (uint32_t *)(tex->img->data + y*tex->img->w*4);
        for( int x = 0; x < (int)tex->img->w; x++ ) {
            uint32_t pix = *row;
            *row++ = (pix&0xff000000)|(~pix&0x00ffffff);
        }
    }
}
static void FlipTex( Texture *tex )
{
    for( int y = 0; y < (int)tex->img->h/2; y++ ) {
        uint32_t *rowt = (uint32_t *)(tex->img->data + y*tex->img->w*4);
        uint32_t *rowb = (uint32_t *)(tex->img->data + (tex->img->h-1-y)*tex->img->w*4);
        for( int x = 0; x < (int)tex->img->w; x++ ) {
            uint32_t pixt = *rowt;
            uint32_t pixb = *rowb;
            *rowt++ = pixb;
            *rowb++ = pixt;
        }
    }
}
static void MirrorTex( Texture *tex )
{
    for( int y = 0; y < (int)tex->img->h; y++ ) {
        uint32_t *coll = (uint32_t *)(tex->img->data + y*tex->img->w*4);
        uint32_t *colr = (uint32_t *)(tex->img->data + y*tex->img->w*4 + (tex->img->w-1)*4);
        for( int x = 0; x < (int)tex->img->w/2; x++ ) {
            uint32_t pixl = *coll;
            uint32_t pixr = *colr;
            *coll++ = pixr;
            *colr-- = pixl;
        }
    }
}

static void Setup(void) {
    puts( "Setup" );
    Scene *s = v7x->scn = V7NewScn(v7x, "MainScene");
    s->dat.light[0].pos = (vec3){ -3.f, 2.f, 9.f };
    s->dat.light[0].val = 3.f;
    s->dat.eye = (vec3){ .0f, .0f, -5.405f };
    s->dat.fov = 102.54f;

    for( int i = 0; i < DEMOPARTS; i++ ) {
        DemoPart *dp = &demo_parts[i];
        demo_models[i] = NewShader(dp->title, dp->fname, dp->data, dp->datalen);
    }

    SDFont *fnt  = &sdf_babylon5; // This is a Babylon 5 shop
    SDFont *fntd = &sdf_unispace;
    // Add these early for blend against black background
    txt_scroll  = NewText( "ScrollText",  fnt,  (vec3){.65f,  .65f,  .3f}, (vec3){ 0.000f,        -4.45f,               1.0f}, (vec4){0.0f, 1.0f, 1.0f, 1.0f} );
    txt_debug1  = NewText( "DebugText1",  fntd, (vec3){.45f,  .45f,  .3f}, (vec3){-8.065f,         3.705f,              1.0f}, (vec4){1.0f, 1.0f, 1.0f, 1.0f} );
    txt_debug1s = NewText( "DebugText1s", fntd, (vec3){.45f,  .45f,  .3f}, (vec3){-8.065f+0.025f,  3.705f      -0.025f, 1.0f}, (vec4){0.0f, 0.0f, 0.0f, 1.0f} );
    txt_debug2  = NewText( "DebugText2",  fntd, (vec3){.45f,  .45f,  .3f}, (vec3){-8.065f,         3.705f-0.35f,        1.0f}, (vec4){1.0f, 1.0f, 1.0f, 1.0f} );
    txt_debug2s = NewText( "DebugText2s", fntd, (vec3){.45f,  .45f,  .3f}, (vec3){-8.065f+0.025f,  3.705f-0.35f-0.025f, 1.0f}, (vec4){0.0f, 0.0f, 0.0f, 1.0f} );
    if( gentitle ) {
        txt_top     = NewText( "TopText",     fnt, (vec3){.825f, .825f, .3f}, (vec3){ 0.000f,  4.04f, 1.0f}, (vec4){0.0f, 1.0f, 1.0f, 1.0f} );
        txt_scrolls = NewText( "ScrollTexts", fnt, (vec3){.650f, .650f, .3f}, (vec3){ 0.000f, -4.45f, 1.0f}, (vec4){0.0f, 0.0f, 0.0f, 1.0f} );
        txt_tops    = NewText( "TopTexts",    fnt, (vec3){.825f, .825f, .3f}, (vec3){ 0.000f,  4.04f, 1.0f}, (vec4){0.0f, 0.0f, 0.0f, 1.0f} );
    }
    fbtex = V7NewTex(v7x, "FBTex");
    fbtex->mag = GL_LINEAR;
    glTextureParameteri( fbtex->id, GL_TEXTURE_MAG_FILTER, fbtex->mag );

    bgmod = V7NewMod(v7x, "BGMod");
    bgmod->prg = V7GetPrg(v7x, "RGB");
    V7Quad(bgmod->msh, 19.83, 19.83/2.0);
    bgmod->t = (vec3){0.f, 0.f, 0.f};
    bgmod->mtr = V7NewMtr(v7x, "BGModMtr");
    bgmod->mtr->coltex = fbtex;
    V7AddMod(s, bgmod);

    swintrek = LoadMod("SSSwinetrek");
    swintrek->s = (vec3){ .5f, .5f, .5f };
    V7AddMod(s, swintrek);
    V7Hide(swintrek);

    pic_bike1   = LoadPicAsMod( s, "../data/smallbike.png", "Bike", (vec3){ 0.505f, 0.505f, 1.0f}, (vec3){ 0.0f, 0.0f,  0.0f } );
    pic_bike2   = LoadPicAsMod( s, "../data/smallbike.png", "ekiB", (vec3){ 0.505f, 0.505f, 1.0f}, (vec3){ 0.0f, 0.0f,  0.0f } );
    pic_bike3   = LoadPicAsMod( s, "../data/smallbike.png", "Mini", (vec3){ 0.505f, 0.505f, 1.0f}, (vec3){ 0.0f, 0.0f,  0.0f } );
    pic_bike4   = LoadPicAsMod( s, "../data/smallbike.png", "Moke", (vec3){ 0.505f, 0.505f, 1.0f}, (vec3){ 0.0f, 0.0f,  0.0f } );
    pic_moi     = LoadPicAsMod( s, "../data/moilogo.png",   "MoI",  (vec3){ 0.935f, 0.935f, 1.0f}, (vec3){ 0.0f, 0.2f,  0.0f } );
    pic_pig     = LoadPicAsMod( s, "../data/usspig.png",    "Pig",  (vec3){ 0.300f, 0.300f, 1.0f}, (vec3){ 0.0f, 5.26f, 0.0f } );
                                  FlipTex( TEXX(pic_bike2) );   V7ImgTex( TEXX(pic_bike2) );
    InvertTex( TEXX(pic_bike3) );                               V7ImgTex( TEXX(pic_bike3) );
    InvertTex( TEXX(pic_bike4) ); MirrorTex( TEXX(pic_bike4) ); V7ImgTex( TEXX(pic_bike4) );

    lines = V7NewLine( v7x, "Lines", 2.0f );
    lines->mtr = V7NewMtr( v7x, "LinesMtr" );
    V7Line( lines->msh, (vec3){ -16.0f, -4.965f, 0.0f }, (vec3){  16.0f, -4.965f, 0.0f } );
    V7Line( lines->msh, (vec3){ -16.0f,  4.965f, 0.0f }, (vec3){  16.0f,  4.965f, 0.0f } );
    V7AddMod( v7x->scn, lines );

    if( !gentitle )
        txt_top     = NewText( "TopText",     fnt,  (vec3){.825f, .825f, .3f}, (vec3){ 0.000f,         4.04f,               1.0f}, (vec4){0.0f, 1.0f, 1.0f, 1.0f} );
    txt_credl   = NewText( "CredTextL", fnt, (vec3){.150f, .150f, .3f}, (vec3){-8.078f, 4.21f, 1.0f}, (vec4){0.7f, 0.7f, 0.7f, 1.0f} );
    txt_credr   = NewText( "CredTextR", fnt, (vec3){.150f, .150f, .3f}, (vec3){ 8.078f, 4.21f, 1.0f}, (vec4){0.7f, 0.7f, 0.7f, 1.0f} );
    txt_quirk   = NewText( "QuirkText", fnt, (vec3){.825f, .825f, .3f}, (vec3){ 0.000f, 0.00f, 1.0f}, (vec4){1.0f, 0.5f, 0.0f, 1.0f} );

    V7CompShd(v7x);

    // Twofield
    gputwofield.eye       = v4_set( 0.0f, 0.0f, 20.0f, 0.0f );
    gputwofield.scale     = 0.65f;
    for( int i = 0; i < LIGHTNUM; i++ ) {
        switch( i%3 ) {
        case 0: gputwofield.lightcol[i] = v4_mul3( v4_set( 1.0f, 0.0f, 0.0f, 0.0f ), 40.0f ); break;
        case 1: gputwofield.lightcol[i] = v4_mul3( v4_set( 0.0f, 1.0f, 0.0f, 0.0f ), 40.0f ); break;
        case 2: gputwofield.lightcol[i] = v4_mul3( v4_set( 0.0f, 0.0f, 1.0f, 0.0f ), 40.0f ); break;
        }
        gputwofield.lightpos[i].x = -12.0f + (24.0f/(LIGHTNUM-1))*i; // fixed, visually moved by yz rot
    }

    // Pigs in Tunnel table instead of slow hash() feast
    // Real Programming got this wrong for Noise3D... damn. Still looked ok, though.
    for( int i = 0; i < TABLELEN; i++ )
        gputunnel.noisetable[i] = v4_set( fract( sinf( (float)((i +   0)&TABLEMASK) ) * 43758.5453123f ),
                                          fract( sinf( (float)((i +  57)&TABLEMASK) ) * 43758.5453123f ),
                                          fract( sinf( (float)((i + 113)&TABLEMASK) ) * 43758.5453123f ),
                                          fract( sinf( (float)((i + 170)&TABLEMASK) ) * 43758.5453123f ) );

    // Knotty knots
    for( int i = 0; i < ROTS; i++ ) {
        gpuknots.rots[i] = v4_cos( v4_add( v4_set( 0, 11, 33, 0 ), v4_set1(i*PIF/2.5f) ) );
    }
    for( int i = 0; i < CORS; i++ ) {
        v3 v = v3_div1( v3_add1( v3_mul1( v3_cos( v3_add( v3_set1( (i/20.0f)*6.3f ), v3_set( 0, 23, 21 ) ) ), 0.5f ), 0.5f ), 160.0f );
        gpuknots.cors[i] = v4_set( v.x, v.y, v.z, 0.0f );
    }

    double t = 0.0;
    for( int i = 0; i < startpart; i++ )
        t += demo_parts[i].duration;
    framectr = (int)round(t*60.0);

    if( pauseframe != -1 ) {
        framectr = pauseframe;
        addframe = 0;
    }

    if( gentitle ) {
        framectr = 10649; // 4.483 in Kaleidonope -> 10944,10757
        addframe = 0;
        txt_top->s    = (vec3){ 1.39f, 1.39f, .3f }; txt_top->t.x    = 0.05f; txt_top->t.y    =  3.160f;
        txt_scroll->s = (vec3){ 1.36f, 1.36f, .3f }; txt_scroll->t.x = 0.05f; txt_scroll->t.y = -3.990f;
        txt_tops->s    = txt_top->s;    txt_tops->t    = txt_top->t;    txt_tops->t.x    -= 0.10f; txt_tops->t.y    -= 0.05f;
        txt_scrolls->s = txt_scroll->s; txt_scrolls->t = txt_scroll->t; txt_scrolls->t.x -= 0.05f; txt_scrolls->t.y -= 0.05f;
        pic_moi->s = (vec3){ 2.00f, 2.00f, 1.00f };
        pic_moi->t.y = 0.0f;
        MirrorTex( TEXX(pic_bike3) ); InvertTex( TEXX(pic_bike3) ); V7ImgTex( TEXX(pic_bike3) );
        MirrorTex( TEXX(pic_bike4) );                               V7ImgTex( TEXX(pic_bike4) );
        pic_bike3->t = (vec3){ -9.24f, -4.255f, 0.00f }; pic_bike3->s = (vec3){ 0.700f, 0.700f, 1.00f };
        pic_bike4->t = (vec3){  9.24f, -4.255f, 0.00f }; pic_bike4->s = (vec3){ 0.700f, 0.700f, 1.00f };
        V7Hide( lines );
    }

    puts("Setup done");
}

static float megafade( float curr, float start, float end, float fadein, float fadeout )
{
    float rv = 1.0f;
         if(                    curr <  start       || curr >  end )          rv = 0.0f;
    else if( fadein  != 0.0f && curr >= start       && curr <= start+fadein ) rv = 1.0f - sinf( PIF/2.0f + ((curr-start)        /fadein )*(PIF/2.0f) ); // in  fast..slow
    else if( fadeout != 0.0f && curr >= end-fadeout && curr <= end          ) rv =        sinf( PIF/2.0f + ((curr-(end-fadeout))/fadeout)*(PIF/2.0f) ); // out slow..fast
    return rv;
}

#define RGBS 7
static const v4 cols[RGBS] = {
    { 1.0f, 0.0f, 0.0f, 1.0f },
    { 1.0f, 1.0f, 0.0f, 1.0f },
    { 0.0f, 1.0f, 0.0f, 1.0f },
    { 0.0f, 1.0f, 1.0f, 1.0f },
    { 0.0f, 0.5f, 1.0f, 1.0f },
    { 1.0f, 0.0f, 1.0f, 1.0f },
    { 1.0f, 0.0f, 0.0f, 1.0f }, // duplicate of top
};
static v4 getRGBfromH( float h )
{
    h = fract( h );
    float Slice     = (RGBS-1) * h;
    float SliceInt  = floorf( Slice );
    float SliceFrac = Slice - SliceInt;
    int i = (int)SliceInt;
    v4 v = v4_mix( cols[i], cols[i+1], SliceFrac );
    return v;
}

///////////////////////////////////////////////////////////////////// LOOP //
static bool Loop(void) {
    static double fps = 0.0;

#ifndef __ANDROID__
    if(v7x->key[_R_])     { v7x->key[_R_]     = 0; V7ReloadShd(v7x); V7CompShd(v7x); V7List(v7x); }
#endif
    if(v7x->key[_ESC_])   { v7x->key[_ESC_]   = 0; return true; }
    if(v7x->key[_Q_])     { v7x->key[_Q_]     = 0; return true; }
    if(v7x->key[_F_])     { v7x->key[_F_]     = 0; V7Fullscreen(v7x); }
    if(v7x->key[_L_])     { v7x->key[_L_]     = 0; V7List(v7x); }
    if(v7x->key[_R_])     { v7x->key[_R_]     = 0; V7ReloadShd(v7x); }
    if(v7x->key[_I_])     { v7x->key[_I_]     = 0; ctrldebug = !ctrldebug; }
    if(v7x->key[_P_])     { v7x->key[_P_]     = 0; addframe = addframe ? 0 : 1; }
    if(v7x->key[_A_])     { v7x->key[_A_]     = 0; framectr = MAXX( framectr-60, 0 );            }
    if(v7x->key[_D_])     { v7x->key[_D_]     = 0; framectr = MIIN( framectr+60, demoframes-1 ); }
    if(v7x->key[_Z_])     { v7x->key[_Z_]     = 0; addframe = 0; framectr = MAXX( framectr-1, 0 );            }
    if(v7x->key[_X_])     { v7x->key[_X_]     = 0; addframe = 0; framectr = MIIN( framectr+1, demoframes-1 ); }

    double demotime = framectr/60.0; // 0.0...end of demo
    float demotimef = (float)demotime;
    int demoframe = (int)round(demotime*60.0);

    // Determine which parttime in which shader should be shown next
    double dtime = 0.0;
    int id;
    for( id = 0; id < DEMOPARTS; id++ ) {
        double dur = demo_parts[id].duration;
        if( demotime < dtime+dur ) break;
        dtime += dur;
    }
    if( id == DEMOPARTS ) return true;

    DemoPart *part = &demo_parts[id];
    double parttime = demotime - dtime; // 0.0..part->duration
    float parttimef = (float)parttime;
    int partw = shaderw ? shaderw : part->width;
    int parth = shaderh ? shaderh : part->height;

    // *** Quick, hide the stash!
    V7Hide( pic_bike1 ); V7Hide( pic_bike2 ); V7Hide( pic_bike3 ); V7Hide( pic_bike4 ); V7Hide( pic_moi ); V7Hide( pic_pig );
    V7Hide( txt_debug1 ); V7Hide( txt_debug1s ); V7Hide( txt_debug2 ); V7Hide( txt_debug2s ); V7Hide( txt_quirk ); V7HideHierarchy( swintrek );

    // *** Shaders global data
    v7x->dat.width  = partw;
    v7x->dat.height = parth;
    v7x->dat.aspect = v7x->dat.width / (float)v7x->dat.height;
    v7x->dat.time   = parttimef;

    // *** Dissolver 3: Hell on Earth
    float fin  = 0.0f, fout  = 0.0f;
    float tfin = 0.0f, tfout = 0.0f;

    switch( part->pid ) {
    case PART_WHITENED:                              tfin = 0.25f; tfout = 0.5f; break;
    case PART_INTRO:                                 tfin = 2.0f;  tfout = 1.0f; break;
    case PART_SPONGE:    fin  = 0.0f;  fout = 0.75f; tfin = 0.5f;  tfout = fout; break;
    case PART_JOURNEY:   fin  = 0.75f; fout = 0.75f; tfin = fin;   tfout = fout; break;
    case PART_ACID:      fin  = 0.5f;  fout = 1.00f; tfin = 1.0f;  tfout = 1.0f; break;
    case PART_QUAT:      fin  = 0.5f;  fout = 1.00f; tfin = fin;   tfout = fout; break;
    case PART_TUNNEL:                                tfin = 1.0f;  tfout = 1.0f; break;
    case PART_GLASSY:                                tfin = 1.0f;  tfout = 1.0f; break;
    case PART_TWOFIELD:  fin = 0.50f;  fout = 0.25f; tfin = 1.0f;  tfout = 1.0f; break;
    case PART_KNOTS:                                 tfin = 1.0f;  tfout = 1.0f; break;
    case PART_KALEIDO:   fin = 0.33f;  fout = 0.66f; tfin = 1.0f;  tfout = 1.0f; break;
    case PART_OUTRO:                                 tfin = 1.0f;  tfout = 2.0f; break;
    case PART_BLACKENED:                             tfin = 0.5f;  tfout = 0.5f; break;
    }

    demo_models[id]->dissolve = 1.0f - megafade( parttimef, 0.0f, (float)part->duration, fin,  fout );
    float textwt = megafade( parttimef, 0.0f, (float)part->duration, tfin, tfout );

    double tm2 = MAXX( 0.0, demotime-demo_parts[0].duration-2.0 );
    v4 colscr = getRGBfromH( (float)(tm2*0.089+0.00) );
    v4 coltop = getRGBfromH( (float)(tm2*0.089+0.33) );
    v4 collin = getRGBfromH( (float)(tm2*0.089+0.66) );

    if( gentitle ) {
        coltop = v4_set( 1.0f, 0.25f, 0.25f, 1.0f );
        colscr = v4_set( 1.0f, 0.25f, 0.25f, 1.0f );
        collin = v4_set( 0.0f, 1.00f, 1.0f, 1.0f );
        part->scrolltext = "\x02""NVidia Jetson Nano\n";
        part->toptext    = "\x02""The Xmas Demo 2024\n";
        V7Show( pic_moi );
        V7Show( pic_bike3 ); V7Show( pic_bike4 );
    } else {
        txt_scroll->t.x = 9.266f - parttimef*5.0f;
    }

    V7Txt( txt_scroll, part->scrolltext );
    V7Txt( txt_top,    part->toptext );
    if( gentitle ) {
        V7Txt( txt_scrolls, part->scrolltext );
        V7Txt( txt_tops,    part->toptext );
    }
    V7Txt( txt_credl,  part->credtext );
    if( part->pid == PART_OUTRO )
        V7Txt( txt_credr,  "\x03""\n""\x03""(%d*%d) ortuO\n", partw, parth );
    else
        V7Txt( txt_credr,  "\x03""\n""\x03""%s (%d*%d)\n", part->title, partw, parth );
    txt_scroll->mtr->col  = (vec4){ colscr.x, colscr.y, colscr.z, 1.0f };
    txt_top->mtr->col     = (vec4){ coltop.x, coltop.y, coltop.z, textwt };
    txt_credl->mtr->col.w = textwt;
    txt_credr->mtr->col.w = textwt;

    lines->mtr->col = (vec4){ collin.x, collin.y, collin.z, megafade( demotimef, 4.0f, (float)(demoduration-4.0), 1.0f, 1.0f ) };

    // *** Console info, display debug
    char inf[80];
    snprintf( inf, sizeof(inf), "%5d %6.3f %7.3f %7.2f %4d*%4d %02d/%02d %s", demoframe, parttime, demotime, fps, v7x->dat.width, v7x->dat.height, id+1, DEMOPARTS, part->title );

    static int previd = -1;
    static double prevtime;
    if( previd != id || fabs(demotime - prevtime) >= 1.0 ) {
        previd = id;
        prevtime = demotime;
        printf( "%s                     \r", inf );
    }

    if( ctrldebug ) {
        const char *hdr = "Frame  Ptime   Ttime     FPS       Res Part";
        V7Txt( txt_debug1, hdr ); V7Txt( txt_debug1s, hdr ); V7Show( txt_debug1 ); V7Show( txt_debug1s );
        V7Txt( txt_debug2, inf ); V7Txt( txt_debug2s, inf ); V7Show( txt_debug2 ); V7Show( txt_debug2s );
    }

    // *** Part controllers
    if( part->pid == PART_WHITENED ) {
        gpugen.in_factor = (float)part->duration-1.0f;
        if( demoframe == 1 ) {
            V7Txt( txt_quirk, "\x02""We must peform a Quirkafleeg\n" ); // JSW2 level, typo in ZX Spectrum version
            txt_quirk->mtr->col = (vec4){ 0.0f, 0.7f, 0.7f, 1.0f };
            txt_quirk->t.y = -0.190f;
        } else {
            V7Txt( txt_quirk, "\x02""The Xmas Demo 2024\n\n""\x02""$99 NVidia Jetson Nano\n" );
            txt_quirk->mtr->col = (vec4){ 1.0f, 0.5f, 0.0f, 1.0f };
            txt_quirk->mtr->col.w = megafade( parttimef, 0.25f, (float)part->duration-0.0f, tfin, tfout );
            txt_quirk->t.y = 0.575f;
        }
        V7Show( txt_quirk );
    }

    if( part->pid == PART_BLACKENED ) {
        gpugen.in_factor = -1.0f;
        if( demoframe == demoframes-2 ) {
            V7Txt( txt_quirk, "\x02""Oh $#!+!The Central Cavern!\n" ); // JSW2 level, duplicate of level 1 in Manic Miner
            txt_quirk->mtr->col = (vec4){ 0.0f, 0.7f, 0.7f, 1.0f };
            txt_quirk->t.y = -0.190f;
        } else {
            V7Txt( txt_quirk, "\x02""See you somewhere in time...\n\n""\x02""Corneliusen & Julin\n" ); // Maiden or Reeve? Take your pick
            txt_quirk->mtr->col = (vec4){ 1.0f, 0.5f, 0.0f, 1.0f };
            txt_quirk->mtr->col.w = megafade( parttimef, 1.0f, (float)part->duration-0.5f, tfin, tfout );
            txt_quirk->t.y = 0.575f;
        }
        V7Show( txt_quirk );
    }

    if( part->pid == PART_INTRO || part->pid == PART_OUTRO ) { // This section will be the topic of a code review some day
        float sfac, wsize, xflip = 1.0f, ptime = parttimef;
        if( part->pid == PART_INTRO ) {
            pic_bike3->dissolve = 1.0f-megafade( parttimef, 1.0f, (float)part->duration-1.0f, 1.0f, 1.0f );
            V7Show( pic_bike3 );
            sfac = parttime < 4.0f ? (float)MAXX( 32.0/((MAXX(parttime-1.0,0.1))*12.0), 1.0 ) : 16.0f - megafade( parttimef, 0.0f, (float)part->duration, 2.0f, 2.0f )*15.0f;
            wsize = MIIN( 0.001f + 0.125f*(MIIN(MAXX(parttimef-1.5f,0.0f)*0.3f,1.0f))*sfac, 0.250f ); // zoom nano fix
        } else {
            pic_bike2->t.x = (-59.00f-1.666664f) + parttimef*6.5f;
            pic_bike2->t.y =   4.45f;
            pic_bike4->dissolve = 1.0f-megafade( parttimef, 2.0f, (float)part->duration-2.0f, 1.0f, 1.0f );
            V7Show( pic_bike2 ); V7Show( pic_bike4 );
            sfac = parttime < part->duration-4.0f ? (float)MIIN(parttime*0.4, 1.0) : 32.0f - megafade( parttimef+1.0f, 0.0f, (float)part->duration, 2.0f, 2.0f )*31.0f;
            wsize = MIIN( 0.001f + 0.125f*(parttimef*0.3f)*sfac, 0.5f ); // zoom nano fix
            xflip = -1.0f;
            ptime -= 1.9f;
        }
        gpugaldance.things[0] = v4_set( 0.0f, 0.0f,  60.0f, 0.500f*sfac );
        gpugaldance.things[1] = v4_set( 0.0f, 0.0f, 180.0f, 0.500f*sfac );
        int flcnt = (THINGCNT-2)/2;
        float fldeg = 30.0f;
        float flt = (PIF*2.0f)/flcnt;
        for( int i = 0; i < flcnt; i++ ) {
            float xoff = sinf( (ptime+flt*i) + (PIF)/4.0f )*1.8f*xflip;
            float yoff = cosf( (ptime+flt*i) + (PIF)/4.0f )*0.85f;
            gpugaldance.things[i*2+2+0] = v4_set( xoff,  yoff,   0.0f+i*fldeg, 0.0f );
            gpugaldance.things[i*2+2+1] = v4_set( xoff,  yoff, 120.0f+i*fldeg, 0.0f );
        }
        gpugaldance.smallsize = wsize;
    }

    if( part->pid == PART_SPONGE )
        gpugen.in_factor = 1.0f - megafade( parttimef, 0.0f, (float)part->duration-1.0f, 1.0f, 2.0f );

    if( part->pid == PART_ACID ) {
        gpuacid.in_factor = 1.0f - megafade( parttimef, 0.0f, (float)part->duration-1.0f, 1.0f, 2.0f );
        // Straight outta the shader
        float t2=parttimef*1.75f+10.0f;
        v3 p=v3_set( 0.0f, 0.0f, -100.0f );
        // p.yz *= rot(sin(t2*0.3)*0.2+0.5);
        float a = sinf(t2*0.3f)*0.2f+0.5f; v3 v = p;
        p.y =  cosf(a)*v.y + sinf(a)*v.z;
        p.z = -sinf(a)*v.y + cosf(a)*v.z;
        // p.xz *= rot(t2*0.2);
        a = t2*0.2f; v = p;
        p.x =  cosf(a)*v.x + sinf(a)*v.z;
        p.z = -sinf(a)*v.x + cosf(a)*v.z;

        v3 t0 = v3_set( 0.0f, 30.0f, 60.0f );
        // t.yz *= rot(sin(t2)*0.3-0.2);
        a = sinf(t2)*0.3f-0.2f; v = t0;
        t0.y =  cosf(a)*v.y + sinf(a)*v.z;
        t0.z = -sinf(a)*v.y + cosf(a)*v.z;
        // t.xz *= rot(t2*0.32);
        a = t2*0.32f; v = t0;
        t0.x =  cosf(a)*v.x + sinf(a)*v.z;
        t0.z = -sinf(a)*v.x + cosf(a)*v.z;

        v3 cz = v3_normalize(v3_sub(t0,p));
        v3 cx = v3_normalize( v3_cross( cz, v3_set( 0.0f, 1.0f, 0.0f ) ) );
        v3 cy = v3_normalize( v3_cross( cz, cx ) );
        gpuacid.in_cx = v4_set( cx.x, cx.y, cx.z, 0.0f );
        gpuacid.in_cy = v4_set( cy.x, cy.y, cy.z, 0.0f );
        gpuacid.in_cz = v4_set( cz.x, cz.y, cz.z, 0.0f );
        gpuacid.in_p  = v4_set( p.x, p.y, p.z, 0.0f );
    }

    if( part->pid == PART_JOURNEY ) {
        float tt = parttimef*0.275f + 25.0f; // note PI*10 flip
        v3 vuv = v3_set( sinf(tt*0.7f), 1.0f, sinf(tt*0.9f) );
        float camSpeed = tt*0.3f;
        v3 vrp = v3_set( sinf(camSpeed+0.2f)*32.0f, 0.0f, cosf(camSpeed+0.2f)*32.0f );
        float camCenterDist = 2.1f+expf(-20.0f*powf(sinf(tt*0.2f)+1.0f,20.0f))*2.8f;
        v3 prp = v3_set( sinf(camSpeed)*32.0f, 0.0f, cosf(camSpeed)*32.0f );
        camSpeed += sinf(tt*0.3f)*0.5f;

        vrp = v3_set( sinf(camSpeed)*32.0f, 0.0f, cosf(camSpeed)*32.0f );
        v3 camMovP = v3_set( sinf(tt)*camCenterDist, cosf(tt)*camCenterDist, 0.0f );
        v3 c0 = v3_normalize(prp);
        v3 c1 = v3_set( 0.0f, 1.0f, 0.0f );
        v3 c2 = v3_normalize( v3_sub( vrp, prp ) );
        prp = v3_add( prp, v3_set( v3_dot( c0, camMovP ), v3_dot( c1, camMovP ), v3_dot( c2, camMovP ) ) );

        v3 vpn = v3_normalize( v3_sub( vrp, prp ) );
        v3 u   = v3_normalize( v3_cross( vuv, vpn ) );
        v3 v   = v3_cross( vpn, u );
        v3 scr = v3_add( prp, v3_mul1( vpn, 1.5f ) );

        gpujourney.in_prp = v4_set( prp.x, prp.y, prp.z, 0.0f );
        gpujourney.in_u   = v4_set( u.x,   u.y,   u.z,   0.0f );
        gpujourney.in_v   = v4_set( v.x,   v.y,   v.z,   0.0f );
        gpujourney.in_scr = v4_set( scr.x, scr.y, scr.z, 0.0f );
        gpujourney.jtime = tt;
    }

    if( part->pid == PART_QUAT ) {
        if( parttime >= 8.00f && parttime <= 10.20f ) { V7Txt( txt_credl, "Based on\nrecycled crap by Corneliusen" ); txt_top->mtr->col = (vec4){0.0f,1.0f,1.0f,1.0f}; V7Txt( txt_top, "\x02""Return of the Xmas Demo\n" ); } // Xmas Demo VI... need I say more?
        pic_bike1->t.x = -45.40f + parttimef*5.0f;
        pic_bike1->t.y =  -4.45f;
        V7Show( pic_bike1 );

        gpuquat.rot_xz = parttimef*1.1f;
        gpuquat.mu_pos = v4_set( -1.102858,0.220685,0.000000,0.000000 ); // stick
        gpuquat.mu_pos.y += sinf( parttimef*1.87f )*0.15f;
        gpuquat.mu_pos.x += sinf( parttimef*1.87f )*0.25f;
        gpuquat.zoom = 3.0f * megafade( parttimef, 0.0f, (float)part->duration-1.0f, 3.0f, 3.0f );
    }

    if( part->pid == PART_TUNNEL ) {
        V7Show( pic_pig );
        pic_pig->dissolve = 1.0f-megafade( parttimef, 0.0f, (float)part->duration, tfin, tfout );
        if( parttimef <= 2.0f )
            gputunnel.in_factor = 2.5f - (parttimef*0.5f)*1.7f; // linear in... didn't we agree on improving megafade last year?
        else
            gputunnel.in_factor = 2.5f - megafade( parttimef, 0.0f, (float)part->duration, 0.00f, 1.50f )*1.7f;

        V7ShowHierarchy(swintrek);

        for(int i=0; i<swintrek->vchd.num; i++) {
            Model **s = (Model **)swintrek->vchd.arr;
            float d = 1.f - parttimef*0.5f;
            if( d >= .0f ) s[i]->dissolve = d;
            else s[i]->dissolve = .0f;
        }

        static Keyframe trans[] = {
             0.0, {.t = {  0.0f, 0.0f,  0.f}}, 
             5.0, {.t = { -3.5f, 0.0f,  2.5f}},
            10.0, {.t = { -0.5f, 0.0f,  3.8f}},
            13.3, {.t = {  0.2f, 0.0f,  4.1f}},
            14.4, {.t = {  1.4f, 0.0f,  4.3f}},
            15.0, {.t = {  8.5f, 0.0f,  5.5f}},
            20.0, {.t = {  9.0f, 0.0f,  4.5f}},
            24.0, {.t = { 15.0f, 0.0f,  5.0f}},
            30.0, {.t = {  0.0f, 0.0f, -1.0f}},
        };
        static Keyframe rottis[] = {
             0.0f, {.r = { 0.000f, 0.000f, 0.000f, 1.000f } },
             3.0f, {.r = { 0.000f,-0.125f, 0.000f, 0.992f } },
             7.5f, {.r = { 0.000f, 0.661f, 0.000f, 0.750f } },
             9.0f, {.r = { 0.000f, 0.707f, 0.000f, 0.707f } },
            11.0f, {.r = { 0.000f, 0.637f, 0.000f, 0.771f } },
            14.0f, {.r = { 0.000f, 0.588f, 0.000f, 0.809f } },
            20.0f, {.r = { 0.000f, 0.426f, 0.000f, 0.905f } },
        };
        static Anim tanim = { V7ANIM_TRANS,  0, V7ANIM_LOOP, { 0, 0, ARRAYLEN(trans),  sizeof(Keyframe), (uint8_t *)trans  } };
        static Anim ranim = { V7ANIM_ROTATE, 0, V7ANIM_LOOP, { 0, 0, ARRAYLEN(rottis), sizeof(Keyframe), (uint8_t *)rottis } };

        V7Animate( swintrek, &tanim, parttimef );
        V7Animate( swintrek, &ranim, parttimef ); 

        v7x->scn->dat.nLight = 3;
        v7x->scn->dat.light[0].pos = (vec3){ -2.0f, 1.0f, 20.0f };
        v7x->scn->dat.light[1].pos = (vec3){ 110.0f, -500.0f, 300.0f };
        v7x->scn->dat.light[2].pos = (vec3){ 0.0f, 0.0f, 0.0f };
    }

    if( part->pid == PART_TWOFIELD ) {
        // Calculate meatball trajectories
        float minsqrt = 8.0f;
        float tadj = (float)(parttime * 0.40);

       float wa = 1.0f;
       float tt = parttimef;
       if( tt <= 2.0f )
           wa = 2.5f*sinf01( PIF*0.5f + (tt*(1.0f/2.0f)*PIF ) ) + 1.0f;
       else if( tt >= 13.5f )
           wa = CLAAMP( 1.0f - sinf( (tt-13.5f)*(1.0f/2.0f)*PIF*0.5f )*1.1f, 0.0f, 1.0f );

       gputwofield.in_factor = megafade( parttimef, 0.5f, (float)part->duration, 1.5f, 1.5f );

        for( int i = 0; i < TFNUM; i++ ) {
            float fi, x, y, z, d, mind = 5.0;
            fi = i*0.7f;
            x = 1.0f+10.0f*cosf(tadj*1.1f+fi),
            y = 3.7f*sinf(tadj     +fi),
            z = 3.3f*sinf(tadj*2.3f+fi);
            d = sqrtf( MAXX( x*x + z*z, minsqrt ) ); if(d < mind) x*=mind/d, z*=mind/d;
            gputwofield.b1[i] = (v4){x*0.9f*wa, y*0.8f*wa, z*0.9f*wa, 0.0f};

            fi = i*1.2f;
            x = -1.0f-10.0f*cosf(tadj*0.7f+fi),
            y = 4.4f*cosf(tadj*0.4f+fi),
            z = -3.1f*sinf(tadj*1.3f+fi);
            d = sqrtf( MAXX( x*x + z*z, minsqrt ) ); if(d < mind) x*=mind/d, z*=mind/d;
            gputwofield.b2[i] = (v4){x*0.9f*wa, y*0.8f*wa, z*0.9f*wa, 0.0f};
        }

        // Move lights
        float fw = (float)(((parttime+1.50) * 0.45)*0.525);
        for( int i = 0; i < LIGHTNUM; i++ ) {
            float frac = fract( fw + (i*1.0f/(float)LIGHTNUM) );
            gputwofield.lightpos[i].y = 10.0f * sinf( frac * (PIF * 2.0f) );
            gputwofield.lightpos[i].z = 11.0f * cosf( frac * (PIF * 2.0f) );
        }
    }

    if( part->pid == PART_GLASSY ) {
        if( parttime >= part->duration-2.0 )
            gpugen.in_factor = megafade( parttimef, 0.0f, (float)part->duration, 0.0f, 2.0f );
        else
            gpugen.in_factor = MIIN(sinf(MIIN(parttimef*0.75f,PIF/2.0f)),1.0f);
    }

    if( part->pid == PART_KNOTS ) {
        gpuknots.in_factor = megafade( parttimef, 0.0f, (float)part->duration, 2.0f, 4.0f );
        pic_moi->dissolve = 1.0f-megafade( parttimef, 8.0f, 14.0f, 0.5f, 0.5f );
        V7Show( pic_moi );
    }

    if( part->pid == PART_KALEIDO ) {
        if( parttime <= 4.0 )
            gpugen.in_factor = MIIN( parttimef*0.25f, 1.0f );
        else if( parttime >= part->duration-2.0 )
            gpugen.in_factor = 1.0f - (parttimef-((float)part->duration-2.0f))*0.50f;
        else
            gpugen.in_factor = 1.0f;
    }

    V7RenderToTex(v7x, demo_models[id], fbtex, partw, parth );
    V7Render(v7x);

    if( savepics ) {
        char fn[256];
        snprintf( fn, sizeof(fn), SAVEPATH, v7x->dat.frame );
        if( v7x->dat.frame%60 == 0 ) puts(fn);
        V7GrabFrame( v7x, fn );
    }

	framectr += addframe;
    totalframes++;

    static double starttime = 0.0;
    static int startframe = 0;
    if( totalframes-startframe == 60 ) {
        double now = V7Time();
        fps = 60.0/(now-starttime);
        starttime = now;
        startframe = totalframes;
    }

    return false;
}

static void SetCWD(char *rel) {
    char dir[260], div;
    size_t len;
#ifdef __linux__
    len = readlink("/proc/self/exe", dir, sizeof(dir)-1);
    div = '/';
#elif _WIN32
    len = GetModuleFileNameA(NULL, dir, sizeof(dir)-1);
    div = '\\';
#endif
    while(len>0)
    if(dir[--len]==div) {
        if(rel==0) dir[len] = 0; // use exe directory
        else strcpy(&dir[++len], rel); // relative path
        break;
    }
#ifdef __linux__
    int ret = chdir(dir);
#elif _WIN32
    SetCurrentDirectoryA(dir);
#endif
}

static int getval( int argc, char **argv, int pos, int minval, int maxval )
{
    if( pos >= argc ) {
        printf( "Option value missing!\n" );
        return -1;
    }
    int rc = atoi( argv[pos] );
    if( rc < minval || rc > maxval ) {
        printf( "Argument out of range! Got %d, range %d-%d\n", rc, minval, maxval );
        return -1;
    }
    return rc;
}

int main(int argc, char **argv)
{
    bool fullscreen = true;
    bool vsync = true;
    printf( "The Xmas Demo 2024 by Julin & Corneliusen\n" );
    printf( "More info here: https://www.ignorantus.com/xmasdemo/\n" );

    for( int i = 0; i < DEMOPARTS; i++ ) {
        demo_parts[i].duration = demo_parts[i+1].starttime - demo_parts[i].starttime;
        demoduration += demo_parts[i].duration;
    }
    demoframes = (int)(demoduration*60.0);

    for( int i = 1; i < argc; i++ ) {
        if( !strcmp( argv[i], "-h" ) ) {
            printf( "Usage: %s [-w] [-v] [-o] [-d] [-i] [-t] [-l] [-p <n>] [-x <n>] [-s <WxH>]\n", argv[0] );
            printf( "  -w: Run in window\n" );
            printf( "  -v: Disable vsync\n" );
            printf( "  -o: Save frames to %s\n", SAVEPATH );
            printf( "  -d: Shader debug on\n" );
            printf( "  -i: Control debug on\n" );
            printf( "  -t: Generate title picture\n" );
            printf( "  -l: List parts\n" );
            printf( "  -p <n>: Start at part <n>\n" );
            printf( "  -x <n>: Start paused at frame <n>\n" );
            printf( "  -s <WxH>: Force shaders to WxH resolution. Careful!\n" );
            return 0;
        }
        if( !strcmp( argv[i], "-w" ) ) { fullscreen  = false; printf( "Windowed mode enabled\n" );         continue; }
        if( !strcmp( argv[i], "-v" ) ) { vsync       = false; printf( "Vsync disabled\n" );                continue; }
        if( !strcmp( argv[i], "-o" ) ) { savepics    = true;  printf( "Saving frames to %s\n", SAVEPATH ); continue; }
        if( !strcmp( argv[i], "-d" ) ) { shdebug     = true;  printf( "Shader debug on\n" );               continue; }
        if( !strcmp( argv[i], "-i" ) ) { ctrldebug   = true;  printf( "Control debug on\n" );              continue; }
        if( !strcmp( argv[i], "-t" ) ) { gentitle    = true;  printf( "Generating title picture only\n" ); continue; }
        if( !strcmp( argv[i], "-l" ) ) {
            double tot = 0.0;
            int secs = 0, ms = 0;
            printf( "No  Title             Start    Dur    Fr First  Last WxH\n" );
            printf( "--------------------------------------------------------------\n" );
            for(int j = 0; j < DEMOPARTS; j++ ) {
                DemoPart *dp = &demo_parts[j];
                printf( "%2d: %-15s %3d.%03d %6.3f %5d %5d %5d %4d*%4d\n", j+1, dp->title, secs, ms, dp->duration, (int)round(dp->duration*60.0), (int)round(tot*60.0), (int)round((tot+dp->duration)*60.0)-1 , dp->width, dp->height );
                tot += dp->duration;
                secs = (int)tot;
                ms   = (int)(fract((float)tot)*1000);
            }
            printf( "--------------------------------------------------------------\n" );
            printf( "    Runtime:        %3d.%03d        %05d\n", secs, ms, (int)round(tot*60.0) );
            return 0;
        }
        if( !strcmp( argv[i], "-p" ) ) {
            startpart = getval( argc, argv, i+1, 1, DEMOPARTS );
            if( startpart == -1 ) return 1;
            printf( "Start at part %d\n", startpart );
            startpart--;
            i++;
            continue;
        }
        if( !strcmp( argv[i], "-x" ) ) {
            pauseframe = getval( argc, argv, i+1, 0, demoframes-1 );
            if( pauseframe == -1 ) return 1;
            printf( "Start paused at frame %d\n", pauseframe );
            i++;
            continue;
        }
        if( !strcmp( argv[i], "-s" ) ) {
            if( i+1 >= argc ) { printf( "Missing argument for -s\n" ); return 0; }
            int rc = sscanf( argv[i+1], "%dx%d", &shaderw, &shaderh );
            if( rc != 2 ) { printf( "Need WxH for -s\n" ); return 0; }
            printf( "Force shader res: %d*%d\n", shaderw, shaderh );
            i++;
            continue;
        }

        printf( "Unknown argument %s!\n", argv[i] );
        return 1;
    }

    SetCWD(0);

    v7x = V7Create(L"A M00se once bit my sister...", 0, 0, 1920, 1080, fullscreen, vsync?1:0, 0);
    if(!v7x) return -1;

    glClearColor( 0, 0, 0, 0 ); 

    V7SET( v7x->flags, V7TOFF );

    Setup();

    double t0 = V7Time();
    while( !Loop() ) ;
    double t1 = V7Time();

    printf( "\n%d frames rendered in %f seconds: %f fps\n\n", totalframes, t1-t0, totalframes/(t1-t0) );
    V7Destroy(v7x);

    return 0;
}
