view src/dmsimple.c @ 2298:b5abfff07ca9

Add new DMGrowBuf helper functions dmGrowBufCopyOffsSize() and dmGrowBufConstCopyOffsSize().
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 04 Jul 2019 10:54:16 +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;
}