view src/dmsimple.c @ 2208:90ec1ec89c56

Revamp the palette handling in lib64gfx somewhat, add helper functions to lib64util for handling external palette file options and add support for specifying one of the "internal" palettes or external (.act) palette file to gfxconv and 64vw.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 14 Jun 2019 05:01:12 +0300
parents 78a0f44aa8b5
children 69a5af2eb1ea
line wrap: on
line source

/*
 * dmlib
 * -- Demo engine "player" code
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2012-2015 Tecnic Software productions (TNSP)
 */
#include <SDL.h>
#include "dmzlib.h"
#include "dmengine.h"
#include "dmargs.h"
#include "dmtext.h"
#include "dmimage.h"
#include "setupfont.h"

#ifdef DM_BUILT_IN_SETUP
#    include "setupimage.h"
#    include "setupmenubar.h"
#endif

// Setup specifics
#define setupTextFieldLen 64

static const char *setupDataName = "SetupData.txt";
static const char *setupFontName = "SetupFont.dmf";
static DMVector
    setupMenuPos, setupMenuDim, setupText1Pos, setupText2Pos,
    setupMenuBarOffs, setupMenuBarDimAdj;
static BOOL setupMenuCenter, setupTextCondensed;
static char
    setupTextFullscreen[setupTextFieldLen],
    setupTextWindowed[setupTextFieldLen],
    setupTextPrefix[setupTextFieldLen],
    setupTextEnterToStart[setupTextFieldLen],
    setupImageName[setupTextFieldLen] = "SetupImage.png",
    setupMenuBarName[setupTextFieldLen] = "SetupMenuBar.png";


// Engine struct
static DMEngineData engine;


static const DMOptArg optList[] =
{
    { 0, '?', "help",       "Show this help", OPT_NONE },
    { 1, 'v', "verbose",    "Be more verbose", OPT_NONE },
};

static const int optListN = sizeof(optList) / sizeof(optList[0]);



static void argShowHelp()
{
    dmPrintBanner(stdout, dmProgName, "[options]");
    dmArgsPrintHelp(stdout, optList, optListN, 0);
}


static BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    (void) optArg;
    (void) currArg;

    switch (optN)
    {
        case 0:
            argShowHelp();
            exit(0);
            break;

        case 1:
            dmVerbosity++;
            break;

        default:
            dmErrorDBGMsg("Unknown option '%s'.\n", currArg);
            return FALSE;
    }

    return TRUE;
}


static inline void dmFillRect(SDL_Surface *screen,
    const int x0, const int y0,
    const int x1, const int y1,
    const Uint32 col)
{
    SDL_Rect rc;
    rc.x = x0;
    rc.y = y0;
    rc.w = x1 - x0 + 1;
    rc.h = y1 - y0 + 1;
    SDL_FillRect(screen, &rc, col);
}


static int engineShowProgress(int loaded, int total)
{
    int dx = 60,
        dh = 20,
        dw = engine.screen->w - (2 * dx),
        dy = (engine.screen->h - dh) / 2;

    // Draw the progress bar
    dmClearSurface(engine.screen, dmMapRGBA(engine.screen, 0,0,0,0));
    dmFillRect(engine.screen, dx, dy, dx+dw, dy+dh, dmMapRGB(engine.screen, 255,255,255));
    dmFillRect(engine.screen, dx+1, dy+1, dx+dw-1, dy+dh-1, dmMapRGB(engine.screen, 0,0,0));

    if (total > 0)
    {
        dmFillRect(engine.screen,
            dx+3, dy+3,
            dx + 3 + ((dw - 3) * loaded) / total,
            dy + dh - 3,
            dmMapRGB(engine.screen, 200,200,200));
    }

    // Flip screen
    SDL_Surface dst;
    SDL_LockTexture(engine.texture, NULL, &dst.pixels, &dst.pitch);

    for (int yc = 0; yc < engine.screen->h; yc++)
    {
        memcpy(dst.pixels + dst.pitch * yc,
            engine.screen->pixels + engine.screen->pitch * yc,
            dst.pitch);
    }

    SDL_UnlockTexture(engine.texture);

    SDL_SetRenderDrawColor(engine.renderer, 0, 0, 0, 255);
    SDL_RenderClear(engine.renderer);
    SDL_RenderCopy(engine.renderer, engine.texture, NULL, NULL);
    SDL_RenderPresent(engine.renderer);

    return DMERR_OK;
}


static int engineLoadResources()
{
    int err, loaded = 0, total = 0;
    BOOL first = TRUE;

    do
    {
        // Show a nice progress bar while loading
        if ((err = engineShowProgress(loaded, total)) != DMERR_OK)
            return err;

        err = dmResourcesPreload(engine.resources, first, &loaded, &total);
        first = FALSE;
    }
    while (err == DMERR_PROGRESS);

    return err;
}


static BOOL engineGenInitializeVideo(int width, int height, int depth, Uint32 flags)
{
    dmPrint(1, "Initializing SDL video %d x %d x %dbpp, flags=0x%08x\n",
        width, height, depth, flags);

    SDL_SetWindowTitle(engine.window, dmProgDesc);

    return TRUE;
}


static BOOL engineInitializeVideo()
{
    return engineGenInitializeVideo(engine.optVidWidth, engine.optVidHeight,
        engine.optVidDepth, engine.optVFlags);
}


typedef struct
{
    int w, h, aspect;
} DMModeEntry;


DMModeEntry *engineModeList = NULL;
int nengineModeList = 0, aengineModeList = 0;


static int engineModeSort(const void *pa, const void *pb)
{
    DMModeEntry
        *va = (DMModeEntry *) pa,
        *vb = (DMModeEntry *) pb;

    return (va->w - vb->w);
}


int engineAddModeToList(int w, int h)
{
    DMModeEntry *mode;
    int i, aspect = engineGetVideoAspect(w, h);

    dmPrint(2, " - Proposed %d x %d\n", w, h);
    if (aspect <= 0)
        return DMERR_INVALID_ARGS;

    // Check if the mode is already in our list
    for (i = 0; i < nengineModeList; i++)
    {
        mode = &engineModeList[i];
        if (mode->w == w && mode->h == h)
            return DMERR_OK;
    }

    // Check if the mode fits our criteria
    switch (engine.optVidSetup)
    {
        case DM_VSETUP_ASPECT:
            if (aspect != engine.optVidAspect)
                return DMERR_OK;
            break;

        case DM_VSETUP_ANY:
            break;
    }

    // Reallocate array if needed
    if (nengineModeList + 1 >= aengineModeList)
    {
        aengineModeList += 16;
        engineModeList = dmRealloc(engineModeList, sizeof(DMModeEntry) * aengineModeList);
        if (engineModeList == NULL)
            return DMERR_MALLOC;
    }

    // Store
    mode = &engineModeList[nengineModeList];
    mode->w = w;
    mode->h = h;
    mode->aspect = engineGetVideoAspect(w, h);
    dmPrint(2, " - %d x %d, %d\n", w, h, mode->aspect);

    nengineModeList++;

    return DMERR_OK;
}


int engineParseSetupConfig(const char *filename)
{
    DMResource *file = NULL;
    int res;
    char buf[256];

    if ((res = dmf_open(engine.resources, filename, &file)) != DMERR_OK)
    {
        dmErrorMsg("Failed to open engine setup configuration '%s', %d: %s.\n",
            filename, res, dmErrorStr(res));
        return res;
    }

    while (dmfgets(buf, sizeof(buf), file) != NULL)
    {
        ssize_t pos;
        // Trim line ending
        for (pos = strlen(buf) - 1; pos >= 0 && isspace(buf[pos]); pos--)
            buf[pos] = 0;

        // Find start of the line
        for (pos = 0; isspace(buf[pos]); pos++);

        // Skip empty lines and comments
        if (buf[pos] == 0 || buf[pos] == '#')
            continue;

        // XXX TODO FIXME: Needs better parsing, with size checks etc.
        char *str = buf+pos;
        if (sscanf(str, "menuPos %f %f",
            &setupMenuPos.x, &setupMenuPos.y) != 2 &&
            sscanf(str, "menuDim %f %f",
            &setupMenuDim.x, &setupMenuDim.y) != 2 &&
            sscanf(str, "text1Pos %f %f",
            &setupText1Pos.x, &setupText1Pos.y) != 2 &&
            sscanf(str, "text2Pos %f %f",
            &setupText2Pos.x, &setupText2Pos.y) != 2 &&
            sscanf(str, "menuBarOffs %f %f",
            &setupMenuBarOffs.x, &setupMenuBarOffs.y) != 2 &&
            sscanf(str, "menuBarDimAdj %f %f",
            &setupMenuBarDimAdj.x, &setupMenuBarDimAdj.y) != 2 &&
            sscanf(str, "menuCenter %d", &setupMenuCenter) != 1 &&
            sscanf(str, "textCondensed %d", &setupTextCondensed) != 1 &&
            sscanf(str, "textFullscreen %s", setupTextFullscreen) != 1 &&
            sscanf(str, "textWindowed %s", setupTextWindowed) != 1 &&
            sscanf(str, "textPrefix %s", setupTextPrefix) != 1 &&
            sscanf(str, "textEnterToStart %s", setupTextEnterToStart) != 1 &&
            sscanf(str, "setupImageName %s", setupImageName) != 1 &&
            sscanf(str, "setupMenuBarName %s", setupMenuBarName) != 1
            )
        {
            res = dmErrorDBG(DMERR_INVALID_DATA,
                "Syntax error in configuration:\n%s\n", buf);
            goto out;
        }
    }

out:
    dmf_close(file);
    return res;
}


static inline DMFloat vsX(DMVector vec)
{
    return (DMFloat) engine.screen->w * vec.x;
}


static inline DMFloat vsY(DMVector vec)
{
    return (DMFloat) engine.screen->h * vec.y;
}


int engineVideoSetup()
{
    DMBitmapFont *menuFont = NULL;
    DMResource *file = NULL;
    SDL_Surface *menuBgImage = NULL, *menuBarImage = NULL;
    int result, menuState = -1;
    BOOL menuFullScreen = TRUE;

    // Compute a list of valid modes
    if (!engineGenInitializeVideo(DM_VSETUP_WIDTH, DM_VSETUP_HEIGHT, engine.optVidDepth, engine.optVFlags))
        goto out;

    SDL_Rect **modes = SDL_ListModes(engine.screen->format, engine.screen->flags | SDL_FULLSCREEN);
    if (modes == (SDL_Rect**) 0)
    {
        dmError(DMERR_INIT_FAIL,
            "No compatible video resolutions/depths available at all. Bailing out.\n");
        goto out;
    }

    if (modes != (SDL_Rect**) -1)
    {
        int i;
        dmPrint(1, "Enumerating modes.\n");
        for (i = 0; modes[i] != NULL; i++)
            engineAddModeToList(modes[i]->w, modes[i]->h);
    }

    if (nengineModeList == 0)
    {
        dmError(DMERR_INIT_FAIL,
            "Umm, no modes found.\n");
        goto out;
    }

    qsort(engineModeList, nengineModeList, sizeof(engineModeList[0]), engineModeSort);

    // Open video temporarily
    if (!engineGenInitializeVideo(DM_VSETUP_WIDTH, DM_VSETUP_HEIGHT, 32, SDL_SWSURFACE | SDL_DOUBLEBUF))
        goto out;

    // Get setup data
    setupMenuPos.x = 0.18750f;
    setupMenuPos.y = 0.41666f;
    setupMenuDim.x = 0.625f;
    setupMenuDim.y = 0.41666f;

    setupMenuBarOffs.x = 0;
    setupMenuBarOffs.y = -0.0001;

    setupMenuBarDimAdj.x = 0;
    setupMenuBarDimAdj.y = 0;

    setupText1Pos.x = 0.3f;
    setupText1Pos.y = 0.9f;

    setupText2Pos.x = 0.25f;
    setupText2Pos.y = 0.85f;

    strcpy(setupTextFullscreen   , "FULLSCREEN");
    strcpy(setupTextWindowed     , " WINDOWED ");
    strcpy(setupTextEnterToStart , "ENTER TO START THE DEMO");
    strcpy(setupTextPrefix       , "USE LEFT/RIGHT ARROW TO TOGGLE : ");

    if (engineParseSetupConfig(setupDataName) != DMERR_OK)
        goto out;

    // Fetch and decompress setup image, try regular resources first
    if ((result = dmf_open(engine.resources, setupImageName, &file)) == DMERR_OK
#ifdef DM_BUILT_IN_SETUP
        || (result = dmf_open_memio(NULL, setupImageName, setupImage, sizeof(setupImage), &file)) == DMERR_OK
#endif
        )
    {
        menuBgImage = dmLoadImage(file);
        dmf_close(file);
    }

    if ((result = dmf_open(engine.resources, setupMenuBarName, &file)) == DMERR_OK
#ifdef DM_BUILT_IN_SETUP
        || (result = dmf_open_memio(NULL, setupMenuBarName, setupMenuBar, sizeof(setupMenuBar), &file)) == DMERR_OK
#endif
        )
    {
        menuBarImage = dmLoadImage(file);
        dmf_close(file);
    }

    if (menuBgImage == NULL || menuBarImage == NULL)
    {
        dmErrorDBGMsg(
            "Could not instantiate setup screen images, %d: %s\n",
            result, dmErrorStr(result));
        goto out;
    }

    if (menuBgImage->w != DM_VSETUP_WIDTH ||
        menuBgImage->h != DM_VSETUP_HEIGHT)
    {
        dmErrorDBGMsg(
            "Setup screen background image does not match "
            "required dimensions (%dx%d vs %dx%d)\n",
            menuBgImage->w, menuBgImage->h,
            DM_VSETUP_WIDTH, DM_VSETUP_HEIGHT);
        goto out;
    }


    // Load up the bitmap font
    if ((result = dmf_open(engine.resources, setupFontName, &file)) == DMERR_OK
#ifdef DM_BUILT_IN_SETUP
        || (result = dmf_open_memio(NULL, setupFontName, setupFont, sizeof(setupFont), &file)) == DMERR_OK
#endif
        )
    {
        result = dmLoadBitmapFont(file, &menuFont);
        dmf_close(file);
    }
    if (result != DMERR_OK)
    {
        dmErrorDBGMsg(
            "Could not instantiate setup screen font, %d: %s\n",
            result, dmErrorStr(result));
        goto out;
    }

    SDL_Surface *tmp = dmConvertScaledSurface(menuBgImage,
        engine.screen->format, engine.screen->flags,
        engine.screen->w, engine.screen->h);
    if (tmp == NULL)
    {
        dmErrorDBGMsg(
            "Could not convert setup screen background image.\n");
        goto out;
    }

    SDL_FreeSurface(menuBgImage);
    menuBgImage = tmp;

    SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);


    // Enter the main loop of the menu
    char menuStr[256];
    int menuOffset = 0,
        menuIndex = 0,
        menuEntryHeight = menuFont->height + 2,
        menuHeight = vsY(setupMenuDim) / menuEntryHeight;

    menuState = 0;

    engine.startTime = SDL_GetTicks();

    while (!menuState)
    {
        while (SDL_PollEvent(&engine.event))
        switch (engine.event.type)
        {
            case SDL_KEYDOWN:
                switch (engine.event.key.keysym.sym)
                {
                    case SDLK_ESCAPE:
                        menuState = -1;
                        break;

                    case SDLK_RETURN:
                        menuState = 1;
                        break;

                    case SDLK_UP:
                        if (menuIndex > 0)
                            menuIndex--;
                        else
                        if (menuOffset > 0)
                            menuOffset--;
                        break;

                    case SDLK_DOWN:
                        if (menuIndex < menuHeight - 1 && menuOffset + menuIndex < nengineModeList - 1)
                            menuIndex++;
                        else
                        if (menuOffset + menuIndex < nengineModeList - 1)
                            menuOffset++;
                        break;

                    case SDLK_LEFT:
                        menuFullScreen = FALSE;
                        break;

                    case SDLK_RIGHT:
                        menuFullScreen = TRUE;
                        break;

                    case SDLK_SPACE:
                        menuFullScreen = !menuFullScreen;
                        break;

                    default:
                        break;
                }

                break;

            case SDL_QUIT:
                menuState = -1;
                break;
        }

        // Draw frame
        engine.frameTime = SDL_GetTicks();

        // Render the menu
        dmDirectBlitSurface(menuBgImage, engine.screen);

        // XXX/TODO: Some hardcoded bits here ...
        float t = engineGetTimeDT(&engine);
        int index, entry;

        for (index = 0, entry = menuOffset; entry < nengineModeList && index < menuHeight; index++, entry++)
        {
            DMModeEntry *mode = &engineModeList[entry];

            if (entry == menuOffset + menuIndex)
            {
                dmScaledBlitSurface32to32TransparentGA(menuBarImage,
                    vsX(setupMenuPos) + vsX(setupMenuBarOffs),
                    vsY(setupMenuPos) + vsY(setupMenuBarOffs) + (index * menuEntryHeight),
                    vsX(setupMenuDim) + vsX(setupMenuBarDimAdj),
                    menuEntryHeight + vsY(setupMenuBarDimAdj),
                    engine.screen,
                    200 + sin(t * 10.0) * 50);
            }

            snprintf(menuStr, sizeof(menuStr),
                "%4d X %-4d  - %d:%d",
                mode->w, mode->h,
                mode->aspect / 1000,
                mode->aspect % 1000);

            DMFloat posX = setupMenuCenter ?
                2.0f + (vsX(setupMenuDim) - menuFont->width * strlen(menuStr)) / 2.0f :
                2.0f;

            dmDrawBMTextConst(
                engine.screen, menuFont,
                setupTextCondensed, DMD_TRANSPARENT,
                vsX(setupMenuPos) + posX,
                vsY(setupMenuPos) + (index * menuEntryHeight),
                menuStr);
        }

        dmDrawBMTextConst(
            engine.screen, menuFont,
            setupTextCondensed, DMD_TRANSPARENT,
            vsX(setupText2Pos),
            vsY(setupText2Pos),
            setupTextEnterToStart);

        snprintf(menuStr, sizeof(menuStr),
            "%s%s",
            setupTextPrefix,
            menuFullScreen ? setupTextFullscreen : setupTextWindowed);

        dmDrawBMTextConst(
            engine.screen, menuFont,
            setupTextCondensed, DMD_TRANSPARENT,
            vsX(setupText1Pos),
            vsY(setupText1Pos),
            menuStr);

        // Flip screen
        SDL_Flip(engine.screen);
        SDL_Delay(25);
    }

    // Okay, we accepted the selection
    if (menuState == 1)
    {
        DMModeEntry *mode = &engineModeList[menuOffset + menuIndex];
        engine.optVidNative =
            mode->w == engine.optVidWidth &&
            mode->h == engine.optVidHeight;

        engine.optVidWidth = mode->w;
        engine.optVidHeight = mode->h;
        engine.optVidAspect = mode->aspect;
        if (menuFullScreen)
            engine.optVFlags |= SDL_FULLSCREEN;

    }

out:
    SDL_FreeSurface(menuBgImage);
    SDL_FreeSurface(menuBarImage);
    dmFreeBitmapFont(menuFont);

    return menuState;
}


int main(int argc, char *argv[])
{
    int err;
    BOOL initSDL = FALSE;

    dmMemset(&engine, 0, sizeof(engine));

    // Pre-initialization
    if ((err = demoPreInit(&engine)) != DMERR_OK)
        goto out;

    if (!dmArgsProcess(argc, argv, optList, optListN,
        argHandleOpt, NULL, OPTH_BAILOUT))
        return DMERR_INIT_FAIL;

    dmPrint(0,
    "%s\n"
    "%s\n",
    dmProgDesc, dmProgAuthor);

    dmPrint(0,
    "Using libSDL"
#if defined(DM_USE_PACKFS) && defined(DM_USE_ZLIB)
    ", zlib"
#endif
#ifdef DM_USE_TREMOR
    ", Tremor Vorbis codec"
#endif
    " and modified stb_image.\n"
    "See README.txt for more information.\n");

    // Initialize dmZlib
    if ((err = dmZLibInit()) != DMERR_OK)
    {
        dmErrorDBGMsg(
            "Failed to initialize dmzlib: %d, %s.\n",
            err, dmErrorStr(err));
        goto out;
    }

    // Initialize resource subsystem
    dmPrint(1, "Initializing resources subsystem.\n");
    if ((err = dmResourcesInit(&engine.resources,
        engine.optPackFilename, "data/",
        engine.optResFlags, engineClassifier)) != DMERR_OK)
    {
        dmErrorMsg(
            "Could not initialize resource manager, #%d: %s.\n",
            err, dmErrorStr(err));
        goto out;
    }

    // Initialize SDL components
    dmPrint(1, "Initializing libSDL.\n");
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0)
    {
        dmError(DMERR_INIT_FAIL,
            "Could not initialize SDL: %s\n",
            SDL_GetError());
        goto out;
    }
    initSDL = TRUE;

    // Set start time
    engine.startTimeAudio = -1;
    engine.startTime = -1;

    // Present video mode selector
    if (engine.optVidAspect <= 0)
        engine.optVidAspect = engineGetVideoAspect(engine.optVidWidth, engine.optVidHeight);

    if (engine.optVidSetup)
    {
        if ((err = engineVideoSetup()) <= 0)
            goto out;
    }

    // Initialize audio parts
    if (engine.optAfmt.freq == 0 && engine.optAfmt.channels == 0)
    {
        engine.optAfmt.freq     = 44100;
        engine.optAfmt.format   = AUDIO_S16SYS;
        engine.optAfmt.channels = 2;
    }

    if (engine.optAfmt.samples == 0)
        engine.optAfmt.samples = engine.optAfmt.freq / 50;

    switch (engine.optAudioSetup)
    {
        case DM_ASETUP_JSS:
#ifdef DM_USE_JSS
            jssInit();

            switch (engine.optAfmt.format)
            {
                case AUDIO_S16SYS: engine.jssFormat = JSS_AUDIO_S16; break;
                case AUDIO_U16SYS: engine.jssFormat = JSS_AUDIO_U16; break;
                case AUDIO_S8:     engine.jssFormat = JSS_AUDIO_S8; break;
                case AUDIO_U8:     engine.jssFormat = JSS_AUDIO_U8; break;
            }

            dmPrint(1, "Initializing miniJSS mixer with fmt=%d, chn=%d, freq=%d\n",
                engine.jssFormat, engine.optAfmt.channels, engine.optAfmt.freq);

            if ((engine.jssDev = jvmInit(engine.jssFormat, engine.optAfmt.channels, engine.optAfmt.freq, JMIX_AUTO)) == NULL)
            {
                dmErrorDBGMsg(
                    "jvmInit() returned NULL, voi perkele.\n");
                goto out;
            }

            if ((engine.jssPlr = jmpInit(engine.jssDev)) == NULL)
            {
                dmErrorDBGMsg(
                    "jmpInit() returned NULL\n");
                goto out;
            }
#else
            dmErrorDBGMsg("miniJSS support not included.\n");
#endif
            break;

        case DM_ASETUP_TREMOR:
#ifndef DM_USE_TREMOR
            dmErrorDBGMsg("Tremor support not included.\n");
#endif
            break;
    }

    // Initialize SDL audio
    dmPrint(1, "Trying to init SDL audio with: fmt=%d, chn=%d, freq=%d, samples=%d\n",
        engine.optAfmt.format, engine.optAfmt.channels,
        engine.optAfmt.freq, engine.optAfmt.samples);

    if ((err = engineInitAudioParts(&engine)) != DMERR_OK)
    {
        dmErrorMsg(
            "engineInitAudioParts() failed: #%d: %s\n",
            err, dmErrorStr(err));
        goto out;
    }

    // Initialize SDL video
    if (engine.demoInitPreVideo != NULL &&
       (err = engine.demoInitPreVideo(&engine)) != DMERR_OK)
    {
        dmErrorMsg(
            "demoInitPreVideo() failed, #%d: %s\n",
            err, dmErrorStr(err));
        goto out;
    }

    if (!engineInitializeVideo())
        goto out;

    if (engine.demoInitPostVideo != NULL &&
       (err = engine.demoInitPostVideo(&engine)) != DMERR_OK)
    {
        dmErrorDBGMsg(
            "demoInitPostVideo() failed, #%d: %s\n",
            err, dmErrorStr(err));
        goto out;
    }

    // Hide cursor
    SDL_ShowCursor(SDL_DISABLE);

    // Load resources
    dmPrint(1, "Loading resources, please wait...\n");
    if ((err = engineLoadResources()) != DMERR_OK)
    {
        dmErrorMsg(
            "Error loading resources, #%d: %s.\n",
            err, dmErrorStr(err));
        goto out;
    }

    // Final initializations
    dmPrint(1, "Initializing demo...\n");
    if ((err = engine.demoInit(&engine)) != DMERR_OK)
    {
        dmErrorMsg(
            "Failure in demoInit(), #%d: %s\n",
            err, dmErrorStr(err));
        goto out;
    }

    // Initialize effects
    if ((err = engineInitializeEffects(&engine)) != DMERR_OK)
    {
        dmErrorMsg(
            "Effects initialization failed, #%d: %s\n",
            err, dmErrorStr(err));
        goto out;
    }

    // Use a timeline, if set
#ifdef DM_USE_TIMELINE
    if (engine.timeline != NULL)
    {
        if ((err = dmLoadTimeline(engine.timeline, &engine.tl)) != DMERR_OK)
        {
            dmErrorDBGMsg(
                "Error loading timeline, #%d: %s\n",
                err, dmErrorStr(err));
            goto out;
        }

        if ((err = dmPrepareTimeline(engine.tl, engine.ptl)) != DMERR_OK)
        {
            dmErrorDBGMsg(
                "Error creating prepared timeline, #%d: %s\n",
                err, dmErrorStr(err));
            goto out;
        }
    }
#endif

    dmPrint(1, "Starting up.\n");

    engine.startTime = SDL_GetTicks();

    SDL_LockAudio();
    enginePauseAudio(&engine, 0);
    SDL_UnlockAudio();

    while (!engine.exitFlag)
    {
        while (SDL_PollEvent(&engine.event))
        switch (engine.event.type)
        {
            case SDL_KEYDOWN:
                switch (engine.event.key.keysym.sym)
                {
                    case SDLK_ESCAPE:
                        engine.exitFlag = TRUE;
                        break;

                    case SDLK_SPACE:
                        engine.pauseFlag = !engine.pauseFlag;
                        break;

                    case SDLK_f:
                        engine.optVFlags ^= SDL_FULLSCREEN;
                        if (!engineInitializeVideo())
                            goto out;
                        break;

                    case SDLK_RETURN:
                        if (engine.event.key.keysym.mod & KMOD_ALT)
                        {
                            engine.optVFlags ^= SDL_FULLSCREEN;
                            if (!engineInitializeVideo())
                                goto out;
                        }
                        break;

                    default:
                        break;
                }

                break;

            case SDL_VIDEOEXPOSE:
                break;

            case SDL_QUIT:
                engine.exitFlag = TRUE;
                break;
        }

        // Draw frame
        engine.frameTime = SDL_GetTicks();
        if (engine.pauseFlag != engine.paused)
        {
            engine.paused = engine.pauseFlag;
            engine.pauseTime = engineGetTick(&engine);
        }

        if (engine.paused)
        {
            engine.startTime = engine.frameTime - engine.pauseTime;
        }

        // Call main tick
        if (engine.demoRender != NULL)
        {
            if ((err = engine.demoRender(&engine)) != DMERR_OK)
                goto out;
        }
#ifdef DM_USE_TIMELINE
        else
        {
            if ((err = dmExecuteTimeline(engine.ptl, engine.screen, engineGetTick(&engine))) != DMERR_OK)
                goto out;
        }
#endif

        SDL_Delay(engine.paused ? 100 : 20);

        engine.frameCount++;
    }

    // Print benchmark results
    engine.endTime = SDL_GetTicks();
    dmPrint(1, "%d frames in %d ms, fps = %1.3f\n",
        engine.frameCount, engine.endTime - engine.startTime,
        (float) (engine.frameCount * 1000.0f) / (float) (engine.endTime - engine.startTime));

    dmPrint(1, "startTime=%d, startTimeAudio=%d, offsetTime=%d\n",
        engine.startTime, engine.startTimeAudio, engine.offsetTime);

out:

    dmPrint(1, "Shutting down.\n");
    SDL_ShowCursor(SDL_ENABLE);

    SDL_LockAudio();
    enginePauseAudio(&engine, 1);
#ifdef DM_USE_JSS
    if (engine.optAudioSetup == DM_ASETUP_JSS)
    {
        jmpClose(engine.jssPlr);
        jvmClose(engine.jssDev);
        jssClose();
    }
#endif

    if (engine.audioSimThread != NULL)
    {
        dmMutexLock(engine.audioStreamMutex);
        engine.audioSimDone = TRUE;
        dmMutexUnlock(engine.audioStreamMutex);
        SDL_WaitThread(engine.audioSimThread, NULL);
    }

    SDL_UnlockAudio();

    if (engine.audioStreamMutex != NULL)
        dmDestroyMutex(engine.audioStreamMutex);

#ifdef DM_USE_TIMELINE
    dmFreeTimeline(engine.tl);
    dmFreePreparedTimelineData(engine.ptl);
#endif

    engineShutdownEffects(&engine);
    dmResourcesClose(engine.resources);

    if (engine.demoShutdown != NULL)
        engine.demoShutdown(&engine);

    if (initSDL)
        SDL_Quit();

    if (engine.demoQuit != NULL)
        engine.demoQuit(&engine);

    dmZLibClose();

    return 0;
}