view tools/64vw.c @ 2610:a9aef6911f7b

Improve image size/window size handling in 64vw to be actually sane. Image now scales correctly (using PAL pixel aspect ratio) vs actual window size. Also toggling between windowed and fullscreen no longer messes up the window size.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 27 Nov 2023 10:14:07 +0200
parents a4f6584edca9
children 97810e89cad5
line wrap: on
line source

/*
 * 64vw - Displayer for various C64 graphics formats via libSDL
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2012-2021 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "dmlib.h"
#include "dmargs.h"
#include "dmfile.h"
#include "libgfx.h"
#include "lib64gfx.h"
#include "lib64util.h"
#include <SDL.h>


#define SET_SKIP_AMOUNT 10


int     optWindowFlags = 0;
int     optSetWindowWidth, optSetWindowHeight;
int     optForcedFormat = -1;
bool    optInfoOnly  = false,
        optProbeOnly = false,
        optListOnly  = false;
size_t  noptFilenames1 = 0, noptFilenames2 = 0;
char    **optFilenames = NULL;
const char *optCharROMFilename = NULL;
DMC64Palette *optC64Palette = NULL;
char    *optC64PaletteFile = NULL;

DMC64MemBlock setCharROM;


static const DMOptArg optList[] =
{
    {  0, '?', "help"            , "Show this help", OPT_NONE },
    {  1,   0, "license"         , "Print out this program's license agreement", OPT_NONE },
    {  2, 'v', "verbose"         , "Be more verbose", OPT_NONE },

    { 10,   0, "fs"              , "Fullscreen", OPT_NONE },
    { 12, 'S', "scale"           , "Scale image by factor (1-10)", OPT_ARGREQ },
    { 14, 'f', "format"          , "Force input format (see --formats)", OPT_ARGREQ },
    { 16, 'F', "formats"         , "List supported input formats", OPT_NONE },
    { 18, 'i', "info"            , "Print information only (no display)", OPT_NONE },
    { 20, 'l', "list"            , "One line per file list of detected format", OPT_NONE },
    { 22, 'P', "probe"           , "Probe only (do not attempt to decode the image)", OPT_NONE },
    { 24,   0, "char-rom"        , "Set character ROM file to be used.", OPT_ARGREQ },
    { 26, 'p', "palette"         , "Set C64 palette to be used (see -p list).", OPT_ARGREQ },
};

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


void dmSetScaleFactor(float factor)
{
    optSetWindowWidth = (int) ((float) D64_SCR_WIDTH * factor * D64_SCR_PAR_XY);
    optSetWindowHeight = (int) ((float) D64_SCR_HEIGHT * factor);
}


void argShowHelp()
{
    dmPrintBanner(stdout, dmProgName, "[options] <input image file(s)>");
    dmArgsPrintHelp(stdout, optList, optListN, 0, 80 - 2);

    fprintf(stdout,
    "\n"
    "Keyboard controls in the viewer:\n"
    "--------------------------------\n"
    "  arrow keys    - next/previous file\n"
    "  space         - next file\n"
    "  home/end      - go to first/last file\n"
    "  page up/down  - go forward/backward %d files\n"
    "  esc / q       - quit\n"
    "  f             - toggle fullscreen\n"
    "\n"
    "Default character ROM file for this build is:\n"
    "  %s\n",
    SET_SKIP_AMOUNT,
    dmGetChargenROMPath()
    );
}


bool argHandleOpt(const int optN, char *optArg, char *currArg)
{
    switch (optN)
    {
        case 0:
            argShowHelp();
            exit(0);
            break;

        case 1:
            dmPrintLicense(stdout);
            exit(0);
            break;

        case 2:
            dmVerbosity++;
            break;

        case 10:
            optWindowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
            break;

        case 12:
            {
                float factor;
                if (sscanf(optArg, "%f", &factor) == 1)
                {
                    if (factor < 1 || factor >= 10)
                    {
                        dmErrorMsg("Invalid scale factor %1.0f, see help for valid values.\n", factor);
                        return false;
                    }

                    dmSetScaleFactor(factor);
                }
                else
                {
                    dmErrorMsg("Invalid scale factor '%s'.\n", optArg);
                    return false;
                }
            }
            break;

        case 14:
            optForcedFormat = -1;
            for (int i = 0; i < ndmC64ImageFormats; i++)
            {
                const DMC64ImageFormat *fmt = &dmC64ImageFormats[i];
                if (fmt->fext != NULL &&
                    strcasecmp(optArg, fmt->fext) == 0)
                {
                    optForcedFormat = i;
                    break;
                }
            }

            if (optForcedFormat < 0)
            {
                dmErrorMsg("Invalid image format argument '%s'.\n", optArg);
                return false;
            }
            break;

        case 16:
            argShowC64Formats(stdout, false, true);
            exit(0);
            break;

        case 20:
            // NOTICE! This fallthrough is intentional for -l option!
            // Take care if reordering the option indices.
            optListOnly = true;
            // Fallthrough

        case 18:
            if (dmVerbosity < 1)
                dmVerbosity = 1;
            optInfoOnly = true;
            break;

        case 22:
            if (dmVerbosity < 1)
                dmVerbosity = 1;
            optProbeOnly = true;
            break;

        case 24:
            optCharROMFilename = optArg;
            break;

        case 26:
            return argHandleC64PaletteOption(optArg, &optC64Palette, &optC64PaletteFile);

        default:
            dmErrorMsg("Unimplemented option argument '%s'.\n", currArg);
            return false;
    }

    return true;
}


bool argHandleFile1(char *filename)
{
    (void) filename;

    noptFilenames1++;
    return true;
}


bool argHandleFile2(char *filename)
{
    if (noptFilenames2 < noptFilenames1)
    {
        optFilenames[noptFilenames2++] = filename;
        return true;
    }
    else
        return false;
}


int dmReadC64Image(const char *filename, const DMC64ImageFormat *forced,
    const DMC64ImageFormat **fmt, DMC64Image **cimage)
{
    Uint8 *dataBuf = NULL;
    size_t dataSize;
    DMGrowBuf tmp;
    int ret;

    if ((ret = dmReadDataFile(NULL, filename, &dataBuf, &dataSize)) != DMERR_OK)
        goto out;

    dmGrowBufConstCreateFrom(&tmp, dataBuf, dataSize);

    if (optProbeOnly)
        ret = dmC64ProbeBMP(&tmp, fmt) != DM_PROBE_SCORE_FALSE ? DMERR_OK : DMERR_NOT_SUPPORTED;
    else
        ret = dmC64DecodeBMP(cimage, &tmp, -1, -1, fmt, forced);

out:
    dmFree(dataBuf);
    return ret;
}


int dmConvertC64ImageToSDLSurface(DMImage **bimage, SDL_Surface **psurf, DMC64Image *cimage, const DMC64ImageConvSpec *spec)
{
    bool charDataSet = false;
    int res;

    if (cimage->charData[0].data == NULL)
    {
        memcpy(&cimage->charData[0], &setCharROM, sizeof(DMC64MemBlock));
        charDataSet = true;
    }

    res = dmC64ConvertBMP2Image(bimage, cimage, spec);

    if (charDataSet)
        memset(&cimage->charData[0], 0, sizeof(DMC64MemBlock));

    if (res == DMERR_OK)
    {
        *psurf = SDL_CreateRGBSurfaceWithFormatFrom(
            (*bimage)->data, (*bimage)->width, (*bimage)->height,
            8, (*bimage)->pitch, SDL_PIXELFORMAT_INDEX8);

        if (*psurf != NULL)
        {
            SDL_SetPaletteColors((*psurf)->format->palette,
                (SDL_Color *) (*bimage)->pal->colors, 0,
                (*bimage)->pal->ncolors);
        }
    }

    return res;
}


int main(int argc, char *argv[])
{
    const DMC64ImageFormat *forced;
    DMC64ImageConvSpec optSpec;
    SDL_Window *window = NULL;
    SDL_Renderer *renderer = NULL;
    SDL_Texture *texture = NULL;
    SDL_Surface *surf = NULL;
    DMImage *bimage = NULL;
    bool initSDL = false, exitFlag, needRedraw, allowResize;
    size_t currIndex, prevIndex;
    int currWindowWidth, currWindowHeight;
    int res = DMERR_OK;

    // Initialize pre-requisites
    if ((res = dmLib64GFXInit()) != DMERR_OK)
    {
        dmErrorMsg("Could not initialize lib64gfx: %s\n",
            dmErrorStr(res));
        goto out;
    }

    dmSetScaleFactor(2.0);
    memset(&optSpec, 0, sizeof(optSpec));
    memset(&setCharROM, 0, sizeof(setCharROM));

    dmInitProg("64vw", "Displayer for various C64 graphics formats", "0.4", NULL, NULL);

    // Parse arguments, round #1
    if (!dmArgsProcess(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile1, OPTH_BAILOUT))
        goto out;

    if (noptFilenames1 == 0)
    {
        argShowHelp();
        res = dmError(DMERR_INVALID_ARGS,
            "No input file(s) specified.\n");
        goto out;
    }

    // Allocate space for filename pointers
    if ((optFilenames = dmCalloc(noptFilenames1, sizeof(char *))) == NULL)
    {
        dmErrorMsg("Could not allocate memory for input file list.\n");
        goto out;
    }

    // Assign the filename pointers
    if (!dmArgsProcess(argc, argv, optList, optListN,
        NULL, argHandleFile2, OPTH_BAILOUT | OPTH_ONLY_OTHER))
        goto out;

    // Check for forced input format
    if (optForcedFormat >= 0)
    {
        forced = &dmC64ImageFormats[optForcedFormat];
        dmMsg(0, "Forced %s format image, type %d, %s\n",
            forced->name, forced->format->mode, forced->fext);
    }
    else
        forced = NULL;

    // If we are simply displaying file information, no need to initialize SDL etc
    if (optInfoOnly || optProbeOnly)
    {
        for (size_t n = 0; n < noptFilenames2; n++)
        {
            char *filename = optFilenames[n];
            const DMC64ImageFormat *fmt = NULL;
            DMC64Image *cimage = NULL;

            res = dmReadC64Image(filename, forced, &fmt, &cimage);
            if (optListOnly)
            {
                fprintf(stdout, "%s | ", filename);
                if (res == DMERR_OK || res == DMERR_NOT_SUPPORTED)
                {
                    fprintf(stdout, "%s [%s]\n",
                        fmt != NULL ? fmt->name : "UNKNOWN",
                        fmt != NULL ? fmt->fext : "???");
                }
                else
                {
                    fprintf(stdout, "ERROR: %s\n",
                        dmErrorStr(res));
                }
            }
            else
            {
                fprintf(stdout, "\n%s\n", filename);
                if (res == DMERR_OK || res == DMERR_NOT_SUPPORTED)
                {
                    dmC64ImageDump(stdout, cimage, fmt, "  ");
                }
                else
                {
                    dmErrorMsg("Could not decode file '%s': %s\n",
                        filename, dmErrorStr(res));
                }
            }

            dmC64ImageFree(cimage);
        }
        goto out;
    }

    if (optC64PaletteFile != NULL)
    {
        if ((res = dmHandleExternalPalette(optC64PaletteFile, &optSpec.pal)) != DMERR_OK)
            goto out;

        if (optSpec.pal->ncolors < D64_NCOLORS)
        {
            dmErrorMsg("Palette does not have enough colors (%d < %d)\n",
                optSpec.pal->ncolors, D64_NCOLORS);
            goto out;
        }
    }
    else
    {
        // No palette file specified, use internal palette
        if (optC64Palette == NULL)
            optC64Palette = &dmC64DefaultPalettes[0];

        dmMsg(1, "Using internal palette '%s' (%s).\n",
            optC64Palette->name, optC64Palette->desc);

        optSpec.cpal = optC64Palette;

        if ((res = dmC64PaletteFromC64Palette(&optSpec.pal, optC64Palette, false)) != DMERR_OK)
        {
            dmErrorMsg("Could not setup palette: %s\n",
                dmErrorStr(res));
            goto out;
        }
    }

    // Check character ROM filename
    if (optCharROMFilename == NULL)
        optCharROMFilename = dmGetChargenROMPath();

    // Attempt to read character ROM
    dmMsg(1, "Using character ROM file '%s'.\n",
        optCharROMFilename);

    if ((res = dmReadDataFile(NULL, optCharROMFilename,
        &setCharROM.data, &setCharROM.size)) != DMERR_OK)
    {
        dmErrorMsg("Could not read character ROM from '%s'.\n",
            optCharROMFilename);
    }

    // Initialize libSDL
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0)
    {
        dmErrorMsg("Could not initialize SDL: %s\n", SDL_GetError());
        goto out;
    }
    initSDL = true;

    // Open window
    if ((window = SDL_CreateWindow(dmProgName,
        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
        optSetWindowWidth, optSetWindowHeight,
        optWindowFlags | SDL_WINDOW_RESIZABLE
        //| SDL_WINDOW_HIDDEN
        )) == NULL)
    {
        dmErrorMsg("Can't create an SDL window: %s\n", SDL_GetError());
        goto out;
    }

    if ((renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC)) == NULL)
    {
        dmErrorMsg("Can't create an SDL renderer: %s\n", SDL_GetError());
        goto out;
    }

//    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");

    // Start main loop
    currWindowWidth = optSetWindowWidth;
    currWindowHeight = optSetWindowHeight;
    currIndex = 0;
    prevIndex = 1;
    needRedraw = true;
    allowResize = true;
    exitFlag = false;
    while (!exitFlag)
    {
        SDL_Event event;
        while (SDL_PollEvent(&event))
        switch (event.type)
        {
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym)
                {
                    case SDLK_ESCAPE:
                    case SDLK_q:
                        exitFlag = true;
                        break;

                    case SDLK_UP:
                    case SDLK_LEFT:
                        if (currIndex > 0)
                            currIndex--;
                        else
                            currIndex = 0;
                        break;

                    case SDLK_SPACE:
                    case SDLK_DOWN:
                    case SDLK_RIGHT:
                        if (currIndex + 1 < noptFilenames2)
                            currIndex++;
                        else
                            currIndex = noptFilenames2 - 1;
                        break;

                    case SDLK_PAGEUP:
                        if (currIndex > SET_SKIP_AMOUNT)
                            currIndex -= SET_SKIP_AMOUNT;
                        else
                            currIndex = 0;
                        break;

                    case SDLK_PAGEDOWN:
                        if (currIndex + 1 + SET_SKIP_AMOUNT < noptFilenames2)
                            currIndex += SET_SKIP_AMOUNT;
                        else
                            currIndex = noptFilenames2 - 1;
                        break;

                    case SDLK_HOME:
                        currIndex = 0;
                        break;

                    case SDLK_END:
                        currIndex = noptFilenames2 - 1;
                        break;

                    case SDLK_f:
                        // If we switch to/from fullscreen, set a flag so we do not
                        // use the fullscreen window size as new stored window size
                        allowResize = false;
                        optWindowFlags ^= SDL_WINDOW_FULLSCREEN_DESKTOP;

                        if (SDL_SetWindowFullscreen(window, optWindowFlags) != 0)
                            goto out;

                        if ((optWindowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == 0)
                            SDL_SetWindowSize(window, optSetWindowWidth, optSetWindowHeight);
                        break;

                    default:
                        break;
                }

                needRedraw = true;
                break;

            case SDL_WINDOWEVENT:
                switch (event.window.event)
                {
                    case SDL_WINDOWEVENT_EXPOSED:
                        needRedraw = true;
                        break;

                    case SDL_WINDOWEVENT_RESIZED:
                        if (allowResize)
                        {
                            optSetWindowWidth  = event.window.data1;
                            optSetWindowHeight = event.window.data2;
                        }
                        currWindowWidth = event.window.data1;
                        currWindowHeight = event.window.data2;
                        allowResize = true;
                        needRedraw = true;
                        break;
                }
                break;

            case SDL_QUIT:
                goto out;
        }

        if (currIndex != prevIndex)
        {
            char *filename = optFilenames[currIndex];
            const DMC64ImageFormat *fmt = NULL;
            DMC64Image *cimage = NULL;
            char *title = NULL;

            // Delete previous surface if any
            if (surf != NULL)
            {
                SDL_FreeSurface(surf);
                surf = NULL;
            }

            if (bimage != NULL)
            {
                dmImageFree(bimage);
                bimage = NULL;
            }

            if ((res = dmReadC64Image(filename, forced, &fmt, &cimage)) != DMERR_OK)
            {
                if (res != DMERR_NOT_SUPPORTED)
                {
                    dmErrorMsg("Could not decode file '%s': %s\n",
                        filename, dmErrorStr(res));
                }
                goto fail;
            }

            if (fmt == NULL || cimage == NULL)
            {
                dmErrorMsg("Probing could not find any matching image format. Perhaps try forcing a format via -f.\n");
                goto fail;
            }

            // Convert image to surface (we are lazy and ugly)
            if (dmConvertC64ImageToSDLSurface(&bimage, &surf, cimage, &optSpec) == DMERR_OK)
            {
                title = dm_strdup_printf("%s - [%d / %d] %s (%dx%d @ %s)",
                    dmProgName,
                    currIndex + 1,
                    noptFilenames2,
                    filename,
                    cimage->fmt->width, cimage->fmt->height,
                    fmt->name);

                if (dmVerbosity >= 1)
                {
                    fprintf(stdout, "\n%s\n", filename);
                    dmC64ImageDump(stdout, cimage, fmt, "  ");
                }
            }

fail:
            dmC64ImageFree(cimage);

            // Create or update surface and texture
            if (surf == NULL && (surf = SDL_CreateRGBSurfaceWithFormat(0,
                D64_SCR_WIDTH, D64_SCR_HEIGHT, 8, SDL_PIXELFORMAT_INDEX8)) == NULL)
            {
                dmErrorMsg("Could not allocate surface.\n");
                goto out;
            }

            if (texture != NULL)
                SDL_DestroyTexture(texture);

            if ((texture = SDL_CreateTextureFromSurface(renderer, surf)) == NULL)
            {
                dmErrorMsg("Could not create texture from surface: %s\n", SDL_GetError());
                goto out;
            }

            // Create stub title string if we didn't manage to decode the image
            if (title == NULL)
            {
                title = dm_strdup_printf("%s - [%d / %d] %s",
                    dmProgName,
                    currIndex + 1,
                    noptFilenames2,
                    filename);
            }

            SDL_SetWindowTitle(window, title);
            dmFree(title);

            needRedraw = true;
            prevIndex = currIndex;
        }

        if (needRedraw)
        {
            // Calculate the render size
            SDL_Rect dstRect;
            dstRect.w = (((float) currWindowHeight) * D64_SCR_FULL_WIDTH / D64_SCR_FULL_HEIGHT / D64_SCR_PAR_XY);
            dstRect.h = currWindowHeight;
            dstRect.x = (currWindowWidth - dstRect.w) / 2;
            dstRect.y = 0;

            SDL_RenderClear(renderer);
            SDL_RenderCopy(renderer, texture, NULL, &dstRect);
            SDL_RenderPresent(renderer);
            needRedraw = false;
        }
        else
            SDL_Delay(50);
    }

out:
    // Cleanup
    dmFree(optFilenames);
    dmC64MemBlockFree(&setCharROM);

    if (texture != NULL)
        SDL_DestroyTexture(texture);

    if (renderer != NULL)
        SDL_DestroyRenderer(renderer);

    if (window != NULL)
        SDL_DestroyWindow(window);

    if (surf != NULL)
        SDL_FreeSurface(surf);

    if (initSDL)
        SDL_Quit();

    dmImageFree(bimage);
    dmPaletteFree(optSpec.pal);
    dmLib64GFXClose();

    return res;
}