// The Xmas Demo 2025: Project Icarus (revised). Control code.
// Authors: Julin and Corneliusen
// Runs in 4Kp60 on Nvidia Jetson AGX Orin. Max out the GPU clock. Duh.
// More info here: https://www.ignorantus.com/pages/xmasdemo2025/
// 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 __linux__
#include <unistd.h>
#else
#include <Windows.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 bool gensnaps = false;
static int pauseframe = -1;
static int startpart = 0;
static bool msaa = false;

static int shaderw, shaderh;
static int dispw, disph;

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

static int framectr;
static int totalframes;

#ifdef __linux__
#define SAVEPATH "../render/"
#else
#define SAVEPATH "..\\..\\tmp\\"
#endif

static V7X *v7x;

static int snapparts[]  = {   0,   1,   1,   2,   3,   3,    4,   5,   6,   6,   6,   7,   8,   9,  10,  11,  11,  12, };
static int snapframes[] = {  30, 252, 587, 158, 476, 515,  864, 411,  25, 649, 937, 749, 630, 434, 276, 162, 326,  50, };
static int snapres[]    = { 512, 512, 512, 512, 512, 256,  512, 512, 256, 512, 256, 512, 512, 512, 512, 512, 256, 512, };
#define SNAPCNT ((int)(sizeof(snapframes)/sizeof(snapframes[0])))
static int currsnap = 0;

static Model *txt_scroll, *txt_scrolls, *txt_top, *txt_tops, *txt_quirk, *txt_quirks, *txt_credl, *txt_credr, *txt_bot, *txt_bots;
static Model *txt_debug1, *txt_debug1s, *txt_debug2, *txt_debug2s;
static Model *pic_sig, *pic_buttoff, *pic_button, *pic_bike1, *pic_bike2, *pic_icarus1, *pic_icarus2, *pic_icarus3, *pic_xmas, *pic_stooge, *pic_chrisr, *pic_player1;
static Model *pic_player2, *pic_turrican, *pic_amiga, *pic_secret, *pic_fist, *pic_pig1, *pic_pig2, *pic_motion, *pic_bug, *pic_igno, *pic_tomb, *pic_moi;

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

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

typedef struct {
    v4 in_scr;
    v4 in_u;
    v4 in_v;
    v4 in_prp;
    float jtime;
    float xfactor;
    float in_factor;
    float paula;
    v4 noisetable[TABLELEN];
} GPUJourney;
static GPUJourney gpujourney;

#define TFNUM     5
#define LIGHTNUM (12*4)
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;

typedef struct {
    v4 ta_in;
    v4 ro_in;
    float scale;
    float nostromo;
    float curly, moe;
} GPUCyber;
static GPUCyber gpucyber;

#define ROTS  7
#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_PITCH,
    PART_BUTTON,
    PART_SPONGE,
    PART_MIDNIGHT,
    PART_JOURNEY,
    PART_TWOFIELD,
    PART_CYBER,
    PART_KALEIDO,
    PART_KNOTS,
    PART_GLASSY,
    PART_CREMATORIA,
    PART_BLACKENED,
    PART_KNOBS,
    BODYPARTS
} PartId;

typedef struct {
    PartId pid;
    char *fname;
    char *title;
    void *data;
    int datalen;
    double starttime;
    char *scrolltext;
    char *toptext;
    char *credtext;
    char *righty;
    double duration; // calculated value
} DemoPart;

#define SPC_CLR "                                                       "
#define TXT_SPON "   4Kp60? Fuck yeah!" SPC_CLR "Do more with more! We're back on the Nvidia Jetson AGX Orin."
#define TXT_SUMM "From Summer Preview 2025" SPC_CLR "This time in real 4K (yeah, we fumbled it in the preview, d'oh)"
#define TXT_JOUR "Music: \"The Agency - End Credits\" by Chris Huelsbeck." SPC_CLR "From the album \"Dressed to Chill\". Get it on bandcamp.com." SPC_CLR "We all played Turrican on our Amiga computers back in the day." SPC_CLR "Duh."
#define TXT_TWOF "Always recycle! And don't waste energy! Don't be a pythoneer." SPC_CLR "                       C: Reducing energy waste since 1972."
#define TXT_CYBE "              Cyber City is back!" SPC_CLR "This time in true 4Kp60." SPC_CLR "Always improve on the perfect! And skip the rubbish." SPC_CLR "\"The future of computer power is pure simplicity.\" - Douglas Adams"
#define TXT_KALE "  Black and blue / And who knows which is which, and who is who? / Up and down / And in the end, it's only round and round, and round" // Us and Them
#define TXT_KNOT "Greetz to Triumph, Scoopex, No Limits, New Wave, Cryptoburners, IT, Spaceballs, Offence, Razor 1911, Crusaders, Kefrens, Cinefex Design, and the rest!"
#define TXT_GLAS "This is what happens you have almost unlimited processing power." SPC_CLR "Get the source code here: ignorantus.com/xmasdemo/" SPC_CLR "Warning: It's C and GLSL. Pythoneers need not apply." SPC_CLR "Duh."
#define TXT_CREM "For long you live and high you fly / But only if you ride the tide / And balanced on the biggest wave / You race towards an early grave" // Breathe

static DemoPart demo_parts[] = {
    { PART_PITCH,      "empty.fs",     "Pitch Black",     &gpugen,      sizeof(gpugen),        0.000f, "",       "",                              "",                     "\x03""C/J/R Productions\n" },
    { PART_BUTTON,     "tunnel.fs",    "Push the Button", &gputunnel,   sizeof(gputunnel),     3.000f, "",       "",                              "meowyih",              "" },
    { PART_SPONGE,     "sponge.fs",    "Spongified",      &gpugen,      sizeof(gpugen),       16.000f, TXT_SPON, "\x02""Spongified 4K\n",         "kithy",                "" },
    { PART_MIDNIGHT,   "midnight.fs",  "Midnight Sun",    &gpugen,      sizeof(gpugen),       31.000f, TXT_SUMM, "\x02""Midnight Sun 4K\n",       "jedilafond",           "\x03""- - . . .   . . . - -\n" },
    { PART_JOURNEY,    "journey.fs",   "Journeyman",      &gpujourney,  sizeof(gpujourney),   46.500f, TXT_JOUR, "\x02""Journeyman 4K\n",         "PauloFalcao\nmeowyih", "" },
    { PART_TWOFIELD,   "twofield.fs",  "Twofield System", &gputwofield, sizeof(gputwofield),  76.000f, TXT_TWOF, "\x02""Twofield System 4K\n",    "w23\nR3N",             "\x03""2716057\n" },
    { PART_CYBER,      "cyber.fs",     "Cyberia",         &gpucyber,    sizeof(gpucyber),     92.000f, TXT_CYBE, "\x02""Cyberia 4K\n",            "Haru86_",              "\x03""Bite my shiny\n""\x03""metal ass\n" },
    { PART_KALEIDO,    "kaleido.fs",   "Kaleidonope",     &gpugen,      sizeof(gpugen),      122.000f, TXT_KALE, "\x02""Kaleidonope 4K\n",        "omansounds",           "\x03""A still tongue\n""\x03""makes a\n""\x03""happy life\n" },
    { PART_KNOTS,      "knots.fs",     "Knots Landing",   &gpuknots,    sizeof(gpuknots),    137.000f, TXT_KNOT, "\x02""Knots Landing 4K\n",      "Elsio",                "", },
    { PART_GLASSY,     "glassy.fs",    "Glassy Knoll",    &gpugen,      sizeof(gpugen),      152.500f, TXT_GLAS, "\x02""Glassy Knoll 4K Ultra\n", "Shane",                "\x03""Apply\n""\x03""yourself\n" },
    { PART_CREMATORIA, "crematoria.fs","Crematoria",      &gpugen,      sizeof(gpugen),      183.000f, TXT_CREM, "\x02""Crematoria 4K\n",         "jamiep",               "\x03""It's turtles all\n""\x03""the way down\n" },
    { PART_BLACKENED,  "noisy.fs",     "Blackened",       &gpugen,      sizeof(gpugen),      198.000f, "",       "",                              "vladstorm",            "" },
    { PART_KNOBS,      "empty.fs",     "Knobs",           &gpugen,      sizeof(gpugen),      207.000f, "",       "",                              "", "" },
    { BODYPARTS,       NULL,           NULL,              NULL,         0,                   212.000f, 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 *mods[256];
static int modcnt = 0;

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 ) { ptr[0] = ' '; ptr[1] = ' '; } }
    m->prg = V7CustPrg(v7x, name, shdsrc, shdname);
    if(gpudata) V7UniBuf(m, gpudata, gpusize);
    m->flags|=V7NMSK;
    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);
    tex->min = GL_LINEAR; glTextureParameteri( tex->id, GL_TEXTURE_MIN_FILTER, tex->min);

    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);
    mods[modcnt++] = 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 );
    mods[modcnt++] = mo;
    return mo;
}

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;

    // bg pics
    pic_icarus1 = LoadPicAsMod( s, "../data/icarus_br.png", "Icarus",  (vec3){ 5.000f, 5.000f, 1.0f}, (vec3){ 0.0f, 0.0f, 0.0f } );
    pic_icarus2 = LoadPicAsMod( s, "../data/icarus.png",    "Icarust", (vec3){ 5.000f, 5.000f, 1.0f}, (vec3){ 0.0f, 0.0f, 0.0f } );
    pic_icarus3 = LoadPicAsMod( s, "../data/icarus.png",    "tsuracI", (vec3){ 5.000f, 5.000f, 1.0f}, (vec3){ 0.0f, 0.0f, 0.0f } );
    pic_motion  = LoadPicAsMod( s, "../data/motion.png",    "Motion",  (vec3){ 7.575f, 7.575f, 1.0f}, (vec3){ 0.0f, 0.0f, 0.0f } );
    pic_bug     = LoadPicAsMod( s, "../data/bug.png",       "Bugge",   (vec3){ 2.075f, 2.075f, 1.0f}, (vec3){ 0.0f, 0.0f, 0.0f } );

    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 still a Babylon 5 shop
    SDFont *fntd = &sdf_unispace;

    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} );
    txt_scroll  = NewText( "ScrollText",  fnt,  (vec3){ .825f, 1.025f, .3f}, (vec3){ 0.000f,        -4.14f,               1.0f}, (vec4){0.0f, 1.0f, 1.0f, 1.0f} );
    txt_scrolls = NewText( "ScrollTexts", fnt,  (vec3){ .825f, 1.025f, .3f}, (vec3){ 0.000f,        -4.14f,               1.0f}, (vec4){0.0f, 0.0f, 0.0f, 1.0f} );
    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} ); // main loop reset
    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 );
    fbtex->min = GL_LINEAR; glTextureParameteri( fbtex->id, GL_TEXTURE_MIN_FILTER, fbtex->min );

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

    // regular pics
    pic_buttoff  = LoadPicAsMod( s, "../data/buttoff.png",   "Butt1",    (vec3){ 2.510f, 2.510f, 1.0f}, (vec3){  0.0f,   0.0f,   0.0f } );
    pic_button   = LoadPicAsMod( s, "../data/button.png",    "Butt2",    (vec3){ 2.510f, 2.510f, 1.0f}, (vec3){  0.0f,   0.0f,   0.0f } );
    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_xmas     = LoadPicAsMod( s, "../data/iffybook.png",  "Iffy",     (vec3){ 3.000f, 3.000f, 1.0f}, (vec3){  0.0f,   0.0f,   0.0f } );
    pic_stooge   = LoadPicAsMod( s, "../data/stooge.png",    "Stooge",   (vec3){ 0.500f, 0.500f, 1.0f}, (vec3){ 10.40f,  5.25f,  0.0f } );
    pic_chrisr   = LoadPicAsMod( s, "../data/chrisr.png",    "Chris",    (vec3){ 0.500f, 0.500f, 1.0f}, (vec3){  9.40f,  5.07f,  0.0f } );
    pic_player1  = LoadPicAsMod( s, "../data/player.png",    "Player",   (vec3){ 1.270f, 1.270f, 1.0f}, (vec3){  0.0f,  -5.01f,  0.0f } );
    pic_player2  = LoadPicAsMod( s, "../data/player.png",    "reyalP",   (vec3){ 1.270f, 1.270f, 1.0f}, (vec3){  0.0f,  -5.01f,  0.0f } );
    pic_turrican = LoadPicAsMod( s, "../data/turrican.png",  "Turrican", (vec3){ 1.260f, 1.260f, 1.0f}, (vec3){  0.0f,   4.30f,  0.0f } );
    pic_amiga    = LoadPicAsMod( s, "../data/amiga.png",     "CBM",      (vec3){ 1.510f, 1.510f, 1.0f}, (vec3){  0.0f,   0.00f,  0.0f } );
    pic_secret   = LoadPicAsMod( s, "../data/secret.png",    "Indy",     (vec3){ 5.580f, 5.580f, 1.0f}, (vec3){  0.0f,   0.00f,  0.0f } );
    pic_fist     = LoadPicAsMod( s, "../data/sbfist.png",    "Fist",     (vec3){ 1.010f, 1.010f, 1.0f}, (vec3){  0.0f,   0.00f,  0.0f } );
    pic_pig1     = LoadPicAsMod( s, "../data/pinkpig.png",   "Pig",      (vec3){ 0.700f, 0.700f, 1.0f}, (vec3){  0.0f,  -4.98f,  0.0f } );
    pic_pig2     = LoadPicAsMod( s, "../data/pinkpig.png",   "giP",      (vec3){ 0.700f, 0.700f, 1.0f}, (vec3){  0.0f,  -4.98f,  0.0f } );
    pic_igno     = LoadPicAsMod( s, "../data/igbanner.png",  "Iggy",     (vec3){ 0.866f, 0.866f, 1.0f}, (vec3){  0.0f,   4.71f,  0.0f } );
    pic_tomb     = LoadPicAsMod( s, "../data/tombstone.png", "Tomb",     (vec3){ 1.000f, 1.000f, 1.0f}, (vec3){  0.0f,  -5.10f,  0.0f } );
    pic_moi      = LoadPicAsMod( s, "../data/moi.png",       "MoI",      (vec3){ 1.510f, 1.510f, 1.0f}, (vec3){  0.0f,   0.0f,   0.0f } );
    pic_sig      = LoadPicAsMod( s, "../data/sig.png",       "Sig",      (vec3){ 0.505f, 0.505f, 1.0f}, (vec3){  8.24f, -5.070f, 0.0f } );

    MirrorTex( pic_bike2->mtr->coltex );   V7ImgTex( pic_bike2->mtr->coltex );
    MirrorTex( pic_player2->mtr->coltex ); V7ImgTex( pic_player2->mtr->coltex );
    MirrorTex( pic_pig2->mtr->coltex );    V7ImgTex( pic_pig2->mtr->coltex );
    MirrorTex( pic_icarus3->mtr->coltex ); V7ImgTex( pic_icarus3->mtr->coltex );

    txt_credl   = NewText( "CredTextL",  fnt, (vec3){   .250f,  .250f, .3f}, (vec3){-8.070f,  4.38f,  1.0f}, (vec4){0.8f, 0.8f, 0.8f, 1.0f} ); // reset main loop
    txt_credr   = NewText( "CredTextR",  fnt, (vec3){   .250f,  .250f, .3f}, (vec3){ 8.078f,  4.38f,  1.0f}, (vec4){1.0f, 1.0f, 1.0f, 1.0f} ); // "
    txt_quirk   = NewText( "QuirkText",  fnt, (vec3){  1.04f,  1.04f,  .3f}, (vec3){ 0.000f,  2.800f, 1.0f}, (vec4){0.0f, 1.0f, 1.0f, 1.0f} );
    txt_quirks  = NewText( "QuirkTexts", fnt, (vec3){  1.04f,  1.04f,  .3f}, (vec3){ 0.000f,  0.000f, 1.0f}, (vec4){0.0f, 0.0f, 0.0f, 1.0f} );
    txt_bot     = NewText( "BoatText",   fnt, (vec3){  1.04f,  1.04f,  .3f}, (vec3){-8.035f, -4.510f, 1.0f}, (vec4){1.0f, 1.0f, 0.0f, 1.0f} );
    txt_bots    = NewText( "BoatTexts",  fnt, (vec3){  1.04f,  1.04f,  .3f}, (vec3){-8.085f, -4.560f, 1.0f}, (vec4){0.0f, 0.0f, 0.0f, 1.0f} );

    V7CompShd(v7x);

    // Twofield 2: The lightening
    gputwofield.eye       = v4_set( 0.0f, 0.0f, 20.0f, 0.0f );
    gputwofield.scale     = 0.65f;
    for( int j = 0; j < LIGHTNUM; j++ ) {
        if( j%16 <= 7 )
            gputwofield.lightcol[j] = v4_mul3( v4_set( 0.0f, 1.0f, 0.0f, 0.0f ), 10.0f );
        else
            gputwofield.lightcol[j] = v4_mul3( v4_set( 1.0f, 0.0f, 0.0f, 0.0f ), 10.0f );
        gputwofield.lightpos[j].x = -12.0f + (24.0f/(LIGHTNUM-1))*(j+0); // fixed, visually moved by yz rot
    }

    // Journey->Tunnel table instead of slow hash() feast
    for( int i = 0; i < TABLELEN; i++ )
        gpujourney.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 ) );
    // Also used in intro tunnel
    memcpy( gputunnel.noisetable, gpujourney.noisetable, sizeof(gpujourney.noisetable) );

    // 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;
    }

    // Move shadows a bit
    txt_scrolls->s = txt_scroll->s; txt_scrolls->t = txt_scroll->t; txt_scrolls->t.x -= 0.05f; txt_scrolls->t.y -= 0.05f;
    txt_quirks->s  = txt_quirk->s;  txt_quirks->t  = txt_quirk->t;  txt_quirks->t.x  -= 0.05f; txt_quirks->t.y  -= 0.05f;

    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;
}

// We solved the HSV edge-color problem: Add a sinus with tops at max color. Duh.
#define KOLCNT 6
static v4 kols[KOLCNT] = {
    {1,0,0,1},
    {1,1,0,1},
    {0,1,0,1},
    {0,1,1,1},
    {1,1,0,1},
    {1,0,0,1},
};
static vec4 getKol( float f )
{
    float fi = floorf( f );
    int i = ((int)fi)%(KOLCNT-1);
    float frac = f-fi;
    float v = sinf01( PIF*0.5f + frac*PIF );
    v4 r = v4_mix( kols[i+1], kols[i+0], v );
    return (vec4){r.x, r.y, r.z, r.w};
}

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

    if( gensnaps ) {
        if( currsnap >= SNAPCNT ) {
            printf( "Snaps done\n" );
            exit( 0 );
        }
        framectr = 0;
        for( int i = 0; i < snapparts[currsnap]; i++ ) {
            framectr += (int)round(demo_parts[i].duration*60.0);
        }
        framectr += snapframes[currsnap];
        v7x->dat.frame = framectr;
    }

    if(v7x->key[_R_])     { v7x->key[_R_]     = 0; V7ReloadShd(v7x); V7CompShd(v7x); V7List(v7x); }
    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[_M_])     { v7x->key[_M_]     = 0; msaa      = !msaa; }
    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
    int demoframe = (int)round(demotime*60.0);

    // We're even more stateless this year! Use -x <frame> to render any frame correctly.
    // 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   partframe = (int)round(parttime*60.0);
    int partw = shaderw ? shaderw : dispw*(msaa?2:1);
    int parth = shaderh ? shaderh : disph*(msaa?2:1);

    // *** Quick, hide the stash!
    for( int i = 0; i < modcnt; i++ )
        V7Hide( mods[i] );

    // *** 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;

    // Reset top text for scrolling in/out
    txt_top->s = (vec3){ 1.000f, 1.000f,  .3f }; txt_tops->s = txt_top->s;
    txt_top->t = (vec3){ 0.000f, 3.930f, 1.0f }; txt_tops->t = (vec3){ txt_top->t.x-0.05f, txt_top->t.y-0.05f, 1.0f };

    // *** Dissolver 4: It never ends, this shit
    float fin = 0.0f, fout = 0.0f, sspeed = 1.4875f;
    switch( part->pid ) {
    case PART_SPONGE:                  fout = 0.75f; break;
    case PART_CYBER:      fin = 2.0f;  fout = 0.75f; break;
    case PART_JOURNEY:    fin = 0.75f; fout = 0.75f; break;
    case PART_TWOFIELD:   fin = 1.00f; fout = 0.50f; sspeed = 1.615f; break;
    case PART_KNOTS:                                 sspeed = 1.700f; break;
    case PART_KALEIDO:                               sspeed = 1.615f; break;
    case PART_BLACKENED:               fout = 2.0f;  break;
    case PART_MIDNIGHT:   fin = 1.0f;  fout = 1.0f;  break;
    case PART_CREMATORIA: fin = 0.5f;                sspeed = 1.615f; 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, 1.0f, 0.75f );

    // Scroll on, scroll off
    float topper = 5.07f;
    txt_top->t.y = topper - (topper-txt_top->t.y) * textwt;
    txt_tops->t.y = txt_top->t.y - 0.05f;

    V7Show( txt_scroll );  V7Txt( txt_scroll,  part->scrolltext ); txt_scroll->mtr->col  = getKol( (float)(demotime*0.33) );
    V7Show( txt_scrolls ); V7Txt( txt_scrolls, part->scrolltext );
    V7Show( txt_top );     V7Txt( txt_top,     part->toptext );    txt_top->mtr->col     = getKol( (float)(demotime*0.33+1.0) );
    V7Show( txt_tops ),    V7Txt( txt_tops,    part->toptext );
    V7Show( txt_credl );   V7Txt( txt_credl,   part->credtext );   txt_credl->mtr->col = (vec4){ 0.8f, 0.8f, 0.8f, textwt };
    V7Show( txt_credr );   V7Txt( txt_credr,   part->righty );     txt_credr->mtr->col = (vec4){ 1.0f, 1.0f, 1.0f, textwt };

    // Scroll dat scroller...
    vec3 *v0 = (vec3*)txt_scroll->msh->vvtx.arr;
    vec3 *v1 = (vec3*)txt_scrolls->msh->vvtx.arr;
    for( int i = 0; i < (int)txt_scroll->msh->vvtx.num; i++ ) {
        float ti = (float)(0.000f + parttimef*sspeed);
        float tii = ti+4.0f/60.0f;
        v0[i].y = v0[i].y - sinf((v0[i].x+ti )/2.f + ti *2.f)/4.5f;
        v1[i].y = v1[i].y - sinf((v1[i].x+tii)/2.f + tii*2.f)/4.5f;
        txt_scroll->t.x  = 12.50f        - ti*3.50f;
        txt_scrolls->t.x = 12.50f+0.055f - ti*3.50f;
        txt_scrolls->t.z = txt_scroll->t.z - 0.00001f;
    }

    // *** Console info, display debug
    char inf[80];
    snprintf( inf, sizeof(inf), "%5d %7.3f %5d %6.3f %7.2f %4d*%4d %02d/%02d %s", demoframe, demotime, partframe, parttime, fps, v7x->dat.width, v7x->dat.height, id, DEMOPARTS-1, 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 = " TFrm   Ttime  Pfrm  Ptime     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_PITCH ) {
        txt_credr->mtr->col.w = megafade( parttimef, 0.0f, (float)part->duration, 0.4f, 0.4f );
        V7Show( pic_bike1 ); pic_bike1->t.x = 0.0f; pic_bike1->t.y = 0.0f; pic_bike1->dissolve = 1.0f - txt_credr->mtr->col.w;
    }

    if( part->pid == PART_BUTTON ) {
        gputunnel.whitey    = MAXX( 1.0f - ((float)part->duration-parttimef), 0.0f );
        gputunnel.in_factor = MAXX( 2.3f - (parttimef/3.2f)*1.1f, 0.935f );

        V7Show( pic_icarus1 ); pic_icarus1->dissolve = 1.0f - megafade( parttimef, 0.0f, 300.00f, 6.0f, 1.0f );
        V7Show( pic_buttoff ); pic_buttoff->dissolve = 1.0f - megafade( parttimef, 0.0f,   3.20f, 1.5f, 0.0f );
        V7Show( pic_button );  pic_button->dissolve  = 1.0f - megafade( parttimef, 3.20f,  6.00f, 0.0f, 1.0f );
        V7Show( pic_bike2 );   pic_bike2->t = (vec3) { 45.00f - parttimef*5.0f, -5.05f, 0.0f };

        char *t;
        if( parttime <= 6.0f ) {
            t = "\x02""\n""\x02""All programs have a\n\n\n\n""\x02""desire to be useful\n"; // From Tron
//            t = "\x02""All programs have a\n\n\n\n\n\n""\x02""desire to be useful\n"; // From Tron
            txt_quirk->mtr->col.w  = megafade( parttimef, 3.20f, 6.00f, 0.5f, 1.0f );
        } else {
            t = "\x02""\n""\x02""Corneliusen / Julin / Ringstad\n\n\n\n""\x02""The Xmas Demo 2025\n";
            txt_quirk->mtr->col.w  = megafade( parttimef, 6.0f, (float)part->duration-0.0f, 1.0f, 0.5f );
            pic_pig1->s = (vec3){ 1.510f, 1.510f, 1.0f };
            pic_pig1->t = (vec3){ 8.0f, -15.0f + sinf01( (parttimef-6.0f)*0.55f )*15.0f, 0.0f };
            V7Show( pic_pig1 );
            pic_pig2->s = pic_pig1->s;
            pic_pig2->t = (vec3){ -pic_pig1->t.x, -pic_pig1->t.y, 0.0f };
            V7Show( pic_pig2 );
        }

        if( gentitle ) {
            txt_top->t.x = -3.300f; txt_tops->t.x = txt_top->t.x-0.05f;
            txt_top->mtr->col = (vec4){1.0f,1.0f,0.0f,1.0f};
            V7Txt( txt_top,  "The Xmas Demo 2025\n" );
            V7Txt( txt_tops, "The Xmas Demo 2025\n" );
            V7Txt( txt_bot,  "4K Ultra HD\n" ); V7Show( txt_bot );
            V7Txt( txt_bots, "4K Ultra HD\n" ); V7Show( txt_bots );
            V7Hide( pic_bike2 );
            txt_quirk->s  = (vec3){ 1.15f, 1.15f, 0.3f }; txt_quirk->t.x  =  0.0f;  txt_quirk->t.y  = 3.20f;
            txt_quirks->s = (vec3){ 1.15f, 1.15f, 0.3f }; txt_quirks->t.x = -0.05f; txt_quirks->t.y = 3.15f;
            t = "\x02""\n""\x02""All programs have a\n\n\n\n""\x02""desire to be useful\n"; // From Tron
        }

        V7Show( txt_quirk );  V7Txt( txt_quirk,  t );
        V7Show( txt_quirks ); V7Txt( txt_quirks, t ); txt_quirks->mtr->col.w = txt_quirk->mtr->col.w;
    }

    if( part->pid == PART_SPONGE ) {
        gpugen.in_factor   = 1.0f - megafade( parttimef, 0.0f, (float)part->duration-1.0f, 1.0f, 2.0f );
        pic_xmas->dissolve = 1.0f - megafade( parttimef, 8.0f, 15.0f,                      0.5f, 0.75f );
        V7Show( pic_xmas );
    }

    if( part->pid == PART_MIDNIGHT ) {
        gpugen.in_factor = 1.0f;
        if( parttimef >= 8.0f && parttimef < 12.0f ) {
            char *t = "\x02""The Xmas Demo Awakens\n"; // Star Wars part 7: The Force Awakens
            V7Txt( txt_top,  t );                      V7Txt( txt_tops, t );
            txt_top->s = (vec3){ 1.20f, 1.20f, 0.3f }; txt_tops->s = txt_top->s;
            txt_top->t = (vec3){ 0.06f, 3.74f, 1.0f }; txt_tops->t = (vec3){ txt_top->t.x-0.05f, txt_top->t.y-0.05f, txt_top->t.z };
        }
    }

    if( part->pid == PART_JOURNEY ) {
        // One day we'll find the loop bug. Today was not that day.
        float tt = parttimef*0.275f;
        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.xfactor = (float)MIIN( part->duration-parttimef, 1.0f );
        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;
        gpujourney.in_factor = 0.75f;

        vec3 *w = (vec3 *)txt_scroll->msh->vvtx.arr;
        pic_player2->t = (vec3){ 114.75f - parttimef*7.55f*0.85f, -5.41f + w[60].y*1.6f, 0.0f };
        pic_player1->t = (vec3){ 151.35f - parttimef*7.55f*0.85f, -4.71f + w[46].y*1.6f, 0.0f };
        V7Show( pic_player2 );
        V7Show( pic_player1 );

        pic_chrisr->dissolve = 1.0f - megafade( parttimef, 0.0f, (float)part->duration, 0.5f, 0.5f );
        V7Show( pic_chrisr );

        pic_turrican->t.y = MIIN( 4.30f, -69.00f + parttimef*4.0f ); // Scrollican
        pic_turrican->dissolve = 1.0f - megafade( parttimef, 0.000f, 25.500f, 1.0f, 1.0f );
        V7Show( pic_turrican );

        float topp = 5.07f;
        float wt = 1.0f - megafade( parttimef, 17.5f, 25.5f, 1.0f, 1.0f );
        txt_top->t.y = topper - (topp-txt_top->t.y) * wt;
        txt_tops->t.y = txt_top->t.y - 0.05f;
    }

    if( part->pid == PART_TWOFIELD ) {
        pic_amiga->t.x = 66.0f - parttimef*7.5f;
        V7Show( pic_amiga );

        // Calculate meatball trajectories, old style
        for( int i = 0; i < TFNUM; i++ ) {
            float t = parttimef*0.5f;
            float fi = i*0.7f; gputwofield.b1[i] = v4_set(  1.0f+10.0f*cosf(t*1.1f+fi), 3.7f*sinf(t    +fi),  2.3f*sinf(t*2.3f+fi), 0.0f );
                  fi = i*1.2f; gputwofield.b2[i] = v4_set( -1.0f-10.0f*cosf(t*0.7f+fi), 4.4f*cosf(t*.4f+fi), -2.1f*sinf(t*1.3f+fi), 0.0f );
        }
        gputwofield.in_factor = 1.0f;

        // Move lights
        float fw = (parttimef+0.50f) * 0.23625f*0.75f;
        bool on = parttimef <= part->duration - 5.0f;
        int lights = on ? (int)MIIN( parttimef*11.0f, LIGHTNUM ) : (int)MAXX( MIIN( LIGHTNUM, (parttimef-(part->duration-5.0f))*10.0f ), 0.0f );
        int i;
        for( i = 0; i < lights; i++ ) {
            float frac = fract( fw + (i*1.0f/(float)LIGHTNUM*2) );
            gputwofield.lightpos[i].y = on ? 10.0f * sinf( frac * (PIF * 2.0f) ) : -100.0f;
            gputwofield.lightpos[i].z = on ? 11.0f * cosf( frac * (PIF * 2.0f) ) : -100.0f;
        }
        for( ; i < LIGHTNUM; i++ ) {
            float frac = fract( fw + (i*1.0f/(float)LIGHTNUM*2) );
            gputwofield.lightpos[i].y = on ? -100.0f : 10.0f * sinf( frac * (PIF * 2.0f) );
            gputwofield.lightpos[i].z = on ? -100.0f : 11.0f * cosf( frac * (PIF * 2.0f) );
        }
    }

    if( part->pid == PART_CYBER ) {
        if( parttimef <= 0.5f ) V7Show( pic_secret );
        if( parttimef >= 26.800f ) V7Show( pic_fist );
        float tt = -3.0f + parttimef*0.80f;
        float wt = tt >= 15.0f ? fmaxf( 1.0f-(tt-15.0f)*0.29f, 0.0f ) : 1.0f;
        gpucyber.ta_in = v4_set( sinf(tt*0.6f*1.14f*0.8571f)*1.8f*wt, cosf(tt*0.5f*1.14f*0.8571f)*1.8f*wt, 0.0f, 0.0f); // Cam dir
        gpucyber.ro_in = v4_set( 0.0f, 0.0f, -5.2f+tt*0.160f, 0.0f ); // Move forwards, not backwards
        gpucyber.scale = tt >= 19.0f ? fmaxf( 0.05f, 1.0f-(tt-19.0f)*0.50f ) : 1.0f;
        gpucyber.nostromo = 3.50f - MIIN( 0.7f, parttimef*0.023f );

        if( parttimef >= 15.0f && parttimef < 19.0f ) {
            char *t = "\x02""Cult of The Xmas Demo\n"; // Cult of Chucky is part 7 in the Child's Play series. AHS: Cult is season 7.
            txt_top->s.x  = 1.327f; txt_top->s.y  = 1.327f; txt_top->t.x  = 0.05f;  txt_top->t.y = 3.73f;
            txt_tops->s.x = 1.327f; txt_tops->s.y = 1.327f; txt_tops->t.x = txt_top->t.x-0.05f; txt_tops->t.y = txt_top->t.y-0.05f;
            V7Txt( txt_top,  t );
            V7Txt( txt_tops, t );
        }
    }

    if( part->pid == PART_KALEIDO ) {
        vec3 *w = (vec3*)txt_scroll->msh->vvtx.arr;
        V7Show( pic_pig2 ); pic_pig2->s = (vec3){1.510f,  1.510f,  1.0f}; pic_pig2->t = (vec3){  14.35f - parttimef*8.15f*0.85f, -4.58f + w[0].y                          *1.6f, 0.0f };
        V7Show( pic_pig1 ); pic_pig1->s = pic_pig2->s;                    pic_pig1->t = (vec3){  89.95f - parttimef*8.15f*0.85f, -4.18f + w[txt_scroll->msh->vvtx.num-1].y*1.6f, 0.0f };
        if( parttime >= part->duration-2.0 ) gpugen.in_factor = 1.0f - (parttimef-((float)part->duration-2.0f))*0.50f;
        else                                 gpugen.in_factor = MIIN( parttimef*0.5f, 1.0f );
    }

    if( part->pid == PART_KNOTS ) {
        gpuknots.in_factor = megafade( parttimef, 0.0f, (float)part->duration, 2.0f, 4.0f );
        pic_motion->s.x = 7.575f + sinf(parttimef*2.5f)*0.75f;
        pic_motion->s.y = 7.575f + cosf(parttimef*2.5f)*0.75f;
        V7Show( pic_motion ); pic_motion->dissolve =  1.0f  -       megafade( parttimef, 0.0f,                       (float)part->duration-7.0f, 1.0f, 1.0f );
        V7Show( pic_bug );    pic_bug->dissolve    =  1.0f  -       megafade( parttimef, (float)part->duration-7.5f, (float)part->duration,      1.0f, 1.0f );
        V7Show( pic_stooge ); pic_stooge->t.x      = 10.40f - 0.95f*megafade( parttimef, 9.0f,                       14.0f,                      0.5f, 0.5f );
    }

    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);

        float topp = 5.07f;
        float wt = 1.0f - megafade( parttimef, 10.80f, 21.0f, 1.0f, 1.0f );
        txt_top->t.y = topper - (topp-txt_top->t.y) * wt;
        txt_tops->t.y = txt_top->t.y - 0.05f;

        pic_igno->t.y = MIIN( 4.71f, -41.65f + parttimef*4.0f );
        pic_igno->dissolve = 1.0f - megafade( parttimef, 0.000f, 21.000f, 1.0f, 1.0f );
        V7Show( pic_igno );
    }

    if( part->pid == PART_CREMATORIA ) {
        vec3 *w = (vec3 *)txt_scroll->msh->vvtx.arr;
        V7Show( pic_tomb ); pic_tomb->t = (vec3){ 88.70f - parttimef*8.15f*0.85f, -4.00f + w[txt_scroll->msh->vvtx.num-1].y*1.6f, 0.0f };
        gpugen.in_factor = (0.3f * megafade( parttimef*0.25f, 0.0f, (float)(part->duration*0.25), 0.5f, 0.5f ));
    }

    if( part->pid == PART_BLACKENED ) {
        char *t;
        if( parttimef >= 5.0f && parttimef <= 6.0f ) {
            t = "\x02""\n""\x02""5202 omeD samX ehT\n\n\n\n""\x02""datsgniR / niluJ / nesuilenroC\n";
            V7Show( pic_icarus3 );
            V7Txt( txt_credr, "\x03""mrotsdalv\n" ); txt_credr->mtr->col = txt_credl->mtr->col;
            V7Txt( txt_credl, "" );
            V7Show( pic_bike2 ); pic_bike2->t  = (vec3){ -13.50f + parttimef*3.5f, -5.05f, 0.0f };
        } else {
            t = "\x02""\n""\x02""The Xmas Demo 2025\n\n\n\n""\x02""Corneliusen / Julin / Ringstad\n";
            V7Show( pic_icarus2 ); pic_icarus2->dissolve = 1.0f - megafade( parttimef, 0.7f, (float)part->duration, 0.5f, 2.0f );
            V7Show( pic_bike1 ); pic_bike1->t  = (vec3){ -13.50f + parttimef*3.5f, -5.05f, 0.0f }; pic_bike1->dissolve = 0.0f;
        }
        txt_quirk->mtr->col.w  = megafade( parttimef, 0.5f, (float)part->duration, 0.5f, 2.0f );
        txt_quirks->mtr->col.w = txt_quirk->mtr->col.w;
        V7Txt( txt_quirk, t );  V7Show( txt_quirk );
        V7Txt( txt_quirks, t ); V7Show( txt_quirks );
    }

    if( part->pid == PART_KNOBS ) {
        V7Show( pic_moi ); pic_moi->dissolve = 1.0f - megafade( parttimef, 0.00f, (float)part->duration-1.5f, 0.25f, 0.25f );
        V7Show( pic_sig ); pic_sig->dissolve = pic_moi->dissolve;
    }

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

    if( savepics ) {
        char fn[256];
             if( gentitle ) snprintf( fn, sizeof(fn), "%sicarus.jpg",     SAVEPATH );
        else if( gensnaps ) snprintf( fn, sizeof(fn), "%ssnap_%02d_%04d_%d.jpg", SAVEPATH, snapparts[currsnap], snapframes[currsnap], snapres[currsnap] );
        else                snprintf( fn, sizeof(fn), "%sout%05d.jpg",    SAVEPATH, framectr );
        if( gentitle || gensnaps || pauseframe != -1 || framectr%60 == 0 ) printf( "%s %d\n", fn, framectr );
        V7GrabFrame( v7x, fn );
        if( gentitle || pauseframe != -1 ) exit( 0 );
        if( gensnaps ) currsnap++;
    }

    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;
        if( savepics ) fps = 60.0f;
    }

    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 = '/';
#else
    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);
#else
    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 uhd = false;
    bool fullscreen = true;
    bool vsync = true;
    printf( "The Xmas Demo 2025 by Corneliusen / Julin / Ringstad\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] [-z] [-m] [-u] [-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( "  -z: Generate snapshots\n" );
            printf( "  -m: I can't believe it's not MSAA on\n" );
            printf( "  -u: UHD\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" ) ) { printf( "Windowed mode enabled\n" );            fullscreen  = false; continue; }
        if( !strcmp( argv[i], "-v" ) ) { printf( "Vsync disabled\n" );                   vsync       = false; continue; }
        if( !strcmp( argv[i], "-o" ) ) { printf( "Saving frames to %s\n", SAVEPATH );    savepics    = true;  continue; }
        if( !strcmp( argv[i], "-d" ) ) { printf( "Shader debug on\n" );                  shdebug     = true;  continue; }
        if( !strcmp( argv[i], "-i" ) ) { printf( "Control debug on\n" );                 ctrldebug   = true;  continue; }
        if( !strcmp( argv[i], "-t" ) ) { printf( "Generating title picture only\n" );    gentitle    = true;  pauseframe = 720; msaa = true;             continue; }
        if( !strcmp( argv[i], "-z" ) ) { printf( "Generating snapshots\n" );             gensnaps    = true;  savepics = true;  msaa = true; uhd = true; continue; }
        if( !strcmp( argv[i], "-m" ) ) { printf( "I can't believe it's not MSAA on\n" ); msaa        = true;  continue; }
        if( !strcmp( argv[i], "-u" ) ) { printf( "UHD on\n" );                           uhd         = true;  continue; }
        if( !strcmp( argv[i], "-l" ) ) {
            double tot = 0.0;
            int secs = 0, ms = 0;
            printf( "No  Title             Start    Dur    Fr First  Last\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\n", j, 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 );
                tot += dp->duration;
                secs = (int)tot;
                ms   = (int)(fract((float)tot)*1000);
            }
            printf( "----------------------------------------------------\n" );
            printf( "%02d  Runtime:        %3d.%03d        %05d\n", DEMOPARTS, secs, ms, (int)round(tot*60.0) );
            return 0;
        }
        if( !strcmp( argv[i], "-p" ) ) {
            startpart = getval( argc, argv, i+1, 0, DEMOPARTS-1 );
            if( startpart == -1 ) return 1;
            printf( "Start at part %d\n", 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 1; }
            int rc = sscanf( argv[i+1], "%dx%d", &shaderw, &shaderh );
            if( rc != 2 ) { printf( "Need WxH for -s\n" ); return 1; }
            printf( "Force shader res: %d*%d\n", shaderw, shaderh );
            i++;
            continue;
        }

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

    SetCWD(0);

    dispw = uhd ? 3840 : 1920;
    disph = uhd ? 2160 : 1080;
    v7x = V7Create(L"A M00se once bit my sister...", 0, 0, dispw, disph, 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;
}
