[BACK]Return to tiles.c CVS log [TXT][DIR] Up to [contributed] / brogue-ce / src / platform

Annotation of brogue-ce/src/platform/tiles.c, Revision 1.1

1.1     ! rubenllo    1: #include <math.h>
        !             2: #include <stdlib.h>
        !             3: #include <SDL_image.h>
        !             4: #include "platform.h"
        !             5: #include "tiles.h"
        !             6:
        !             7: #define PI  3.14159265358979323846
        !             8:
        !             9: #define PNG_WIDTH    2048   // width (px) of the source PNG
        !            10: #define PNG_HEIGHT   5568   // height (px) of the source PNG
        !            11: #define TILE_WIDTH    128   // width (px) of a single tile in the source PNG
        !            12: #define TILE_HEIGHT   232   // height (px) of a single tile in the source PNG
        !            13: #define TILE_ROWS      24   // number of rows in the source PNG
        !            14: #define TILE_COLS      16   // number of columns in the source PNG
        !            15: #define TEXT_X_HEIGHT 100   // height (px) of the 'x' outline
        !            16: #define TEXT_BASELINE  46   // height (px) of the blank space below the 'x' outline
        !            17: #define MAX_TILE_SIZE  64   // maximum width or height (px) of screen tiles before we switch to linear interpolation
        !            18:
        !            19:
        !            20: // How each tile should be processed:
        !            21: //  -  's' = stretch: tile stretches to fill the space
        !            22: //  -  'f' = fit: preserve aspect ratio (but tile can stretch up to 20%)
        !            23: //  -  't' = text: characters must line up vertically (max. stretch 40%)
        !            24: //  -  '#' = symbols: other Unicode characters (max. stretch 40%)
        !            25: static const char TileProcessing[TILE_ROWS][TILE_COLS+1] = {
        !            26:     "ffffffffffffffff", "ssssssssssssssss", "#t##########t#t#", "tttttttttttt###t",
        !            27:     "#ttttttttttttttt", "ttttttttttt#####", "#ttttttttttttttt", "ttttttttttt#####",
        !            28:     "################", "################", "################", "################",
        !            29:     "tttttttttttttttt", "ttttttt#tttttttt", "tttttttttttttttt", "ttttttt#tttttttt",
        !            30:     "ffsfsfsffsssssss", "ssfsfsffffffffff", "fffffffffffffsff", "ffffffffffffffff",
        !            31:     "fsssfffffffffffs", "fsffffffffffffff", "ffffssssffssffff", "ffffsfffffssssff"
        !            32: };
        !            33:
        !            34: typedef struct ScreenTile {
        !            35:     short foreRed, foreGreen, foreBlue; // foreground color (0..100)
        !            36:     short backRed, backGreen, backBlue; // background color (0..100)
        !            37:     short charIndex;    // index of the glyph to draw
        !            38:     short needsRefresh; // true if the tile has changed since the last screen refresh, else false
        !            39: } ScreenTile;
        !            40:
        !            41: static SDL_Window *Win = NULL;      // the SDL window
        !            42: static SDL_Surface *TilesPNG;       // source PNG
        !            43: static SDL_Texture *Textures[4];    // textures used by the renderer to draw tiles
        !            44: static int numTextures = 0;         // how many textures are available in `Textures`
        !            45: static int8_t tilePadding[TILE_ROWS][TILE_COLS];  // how many black lines are at the top/bottom of each tile in the source PNG
        !            46: static boolean tileEmpty[TILE_ROWS][TILE_COLS];   // true if a tile is completely black in the source PNG, else false
        !            47:
        !            48: // How much should a corner of a tile be shifted by before its downscaling, to improve its sharpness.
        !            49: // The first two dimensions are the tile's coordinates (row and column, both zero-based).
        !            50: // The third dimension is either 0 for shifts along the horizontal axis, or 1 for the vertical axis.
        !            51: // The fourth dimention is the downscaling operation's target size (width for horizontal axis, height for vertical).
        !            52: // The last dimension is 0 for top/left, 1 for bottom/right, 2 for center.
        !            53: // The values stored in tileShifts are signed integers. Unit is 1/10th of a pixel.
        !            54: static int8_t tileShifts[TILE_ROWS][TILE_COLS][2][MAX_TILE_SIZE][3];
        !            55:
        !            56: static ScreenTile screenTiles[ROWS][COLS];  // buffer for the expected contents of the screen
        !            57: static int baseTileWidth = -1;      // width (px) of tiles in the smallest texture (`Textures[0]`)
        !            58: static int baseTileHeight = -1;     // height (px) of tiles in the smallest texture (`Textures[0]`)
        !            59:
        !            60:
        !            61: int windowWidth = -1;               // the SDL window's width (in "screen units", not pixels)
        !            62: int windowHeight = -1;              // the SDL window's height (in "screen units", not pixels)
        !            63: boolean fullScreen = false;         // true if the window should be full-screen, else false
        !            64: boolean softwareRendering = false;  // true if hardware acceleration is disabled (by choice or by force)
        !            65:
        !            66:
        !            67: /// Prints the fatal error message provided by SDL then closes the app.
        !            68: static void sdlfatal(char *file, int line) {
        !            69:     fprintf(stderr, "Fatal SDL error (%s:%d): %s\n", file, line, SDL_GetError());
        !            70:     exit(1);
        !            71: }
        !            72:
        !            73:
        !            74: /// Prints the fatal error message provided by SDL_image then closes the app.
        !            75: static void imgfatal(char *file, int line) {
        !            76:     fprintf(stderr, "Fatal SDL_image error (%s:%d): %s\n", file, line, IMG_GetError());
        !            77:     exit(1);
        !            78: }
        !            79:
        !            80:
        !            81: #if !SDL_VERSION_ATLEAST(2, 0, 5)
        !            82:
        !            83: SDL_Surface *SDL_CreateRGBSurfaceWithFormat(Uint32 flags, int width, int height, int depth, Uint32 format) {
        !            84:     Uint32 r, g, b, a;
        !            85:     if (!SDL_PixelFormatEnumToMasks(format, &depth, &r, &g, &b, &a)) sdlfatal(__FILE__, __LINE__);
        !            86:     return SDL_CreateRGBSurface(flags, width, height, depth, r, g, b, a);
        !            87: }
        !            88:
        !            89: #endif
        !            90:
        !            91:
        !            92: /// Returns the numbers of black lines at the top and bottom of a given glyph in the source PNG.
        !            93: ///
        !            94: /// For example, if the glyph has 30 black lines at the top and 40 at the bottom, the function
        !            95: /// returns 30 (the least of the two).
        !            96: ///
        !            97: /// In case the glyph is very small, the function never returns more than TILE_HEIGHT / 4.
        !            98: /// This is to avoid drawing the glyph more than twice its size relative to other glyphs
        !            99: /// when the window's aspect ratio is very large (super wide).
        !           100: ///
        !           101: static int getPadding(int row, int column) {
        !           102:     int padding;
        !           103:     Uint32 *pixels = TilesPNG->pixels; // each pixel is encoded as 0xffRRGGBB
        !           104:     for (padding = 0; padding < TILE_HEIGHT / 4; padding++) {
        !           105:         for (int x = 0; x < TILE_WIDTH; x++) {
        !           106:             int y1 = padding;
        !           107:             int y2 = TILE_HEIGHT - padding - 1;
        !           108:             if (pixels[(x + column * TILE_WIDTH) + (y1 + row * TILE_HEIGHT) * PNG_WIDTH] & 0xffffffU ||
        !           109:                 pixels[(x + column * TILE_WIDTH) + (y2 + row * TILE_HEIGHT) * PNG_WIDTH] & 0xffffffU)
        !           110:             {
        !           111:                 return padding;
        !           112:             }
        !           113:         }
        !           114:     }
        !           115:     return padding;
        !           116: }
        !           117:
        !           118:
        !           119: /// Tells if a tile is completely empty (black) in the source PNG.
        !           120: static boolean isTileEmpty(int row, int column) {
        !           121:     Uint32 *pixels = TilesPNG->pixels; // each pixel is encoded as 0xffRRGGBB
        !           122:     for (int y = 0; y < TILE_HEIGHT; y++) {
        !           123:         for (int x = 0; x < TILE_WIDTH; x++) {
        !           124:             if (pixels[(x + column * TILE_WIDTH) + (y + row * TILE_HEIGHT) * PNG_WIDTH] & 0xffffffU) {
        !           125:                 return false;
        !           126:             }
        !           127:         }
        !           128:     }
        !           129:     return true;
        !           130: }
        !           131:
        !           132:
        !           133: /// Downscales a tile to the specified size.
        !           134: ///
        !           135: /// The downscaling is performed in linear color space, rather than in gamma-compressed space which would cause
        !           136: /// dimming (average of a black pixel and a white pixel should be 50% grey = 0.5 luminance = 187 sRGB, not 128).
        !           137: /// To simplify the computation, we assume a gamma of 2.0.
        !           138: ///
        !           139: /// The tile is logically split into 16 parts (sliced by 3 vertical lines and 3 horizontal ones), so that:
        !           140: ///
        !           141: ///  -  the four corners can be positioned to preserve the tile's aspect ratio,
        !           142: ///     possibly filling the blank spaces at the top and bottom;
        !           143: ///  -  the top, bottom, left, right, and center areas can be independently aligned
        !           144: ///     with output pixels to improve sharpness. This is achieved by adding pre-computed
        !           145: ///     sub-pixel shifts to the target coordinates as we map source pixels to target pixels.
        !           146: ///
        !           147: /// Some tiles, like walls and doors, always stretch to fill the space regardless of aspect ratio.
        !           148: /// Other tiles must preserve aspect ratio, but we allow up to 20% stretch for graphic tiles and
        !           149: /// 40% for text tiles.
        !           150: ///
        !           151: /// For text tiles (letters, digits, punctuation signs), the characters' x-height and baseline
        !           152: /// (corresponding to the top and bottom of "x") are pixel-aligned regardless of outlines.
        !           153: /// This ensures that all letters neatly line up without jumping up and down.
        !           154: /// We also reduce perceived boldness by applying a custom brightness curve;
        !           155: /// it makes the text more legible at the smaller sizes.
        !           156: ///
        !           157: /// Wall tops are diagonal sine waves, approximately 4 pixels apart.
        !           158: ///
        !           159: /// \param surface the target surface
        !           160: /// \param tileWidth width (px) of tiles in the target surface
        !           161: /// \param tileHeight height (px) of tiles in the target surface
        !           162: /// \param row row (zero-based) on which the tile is located in both the source PNG and the target surface
        !           163: /// \param column column (zero-based) in which the tile is located in both the source PNG and the target surface
        !           164: /// \param optimizing pass true when optimizing tiles, else false
        !           165: /// \return estimated amount of blur in the resulting tile (when optimizing)
        !           166: ///
        !           167: static double downscaleTile(SDL_Surface *surface, int tileWidth, int tileHeight, int row, int column, boolean optimizing) {
        !           168:     int8_t noShifts[3] = {0, 0, 0};
        !           169:     int padding = tilePadding[row][column];         // how much blank spaces there is at the top and bottom of the source tile
        !           170:     char processing = TileProcessing[row][column];  // how should this tile be processed?
        !           171:
        !           172:     // Size of the area the glyph must fit into
        !           173:     int fitWidth = max(1, baseTileWidth);
        !           174:     int fitHeight = max(1, baseTileHeight);
        !           175:
        !           176:     // Number of sine waves that can fit in the tile (for wall tops)
        !           177:     const int numHorizWaves = max(2, min(6, round(fitWidth * .25)));
        !           178:     const int numVertWaves = max(2, min(11, round(fitHeight * .25)));
        !           179:
        !           180:     // Size of the downscaled glyph
        !           181:     int glyphWidth, glyphHeight;
        !           182:
        !           183:     // accumulator for pixel values (linear color space), encoded as
        !           184:     // 0xCCCCCCCCSSSSSSSS where C is a counter and S is a sum of squares
        !           185:     uint64_t *values = malloc(tileWidth * tileHeight * sizeof(uint64_t));
        !           186:     memset(values, 0, tileWidth * tileHeight * sizeof(uint64_t));
        !           187:     double blur = 0;
        !           188:
        !           189:     // if the tile is empty, we can skip the downscaling
        !           190:     if (tileEmpty[row][column]) goto downscaled;
        !           191:
        !           192:     // decide how large we can draw the glyph
        !           193:     if (processing == 's' || optimizing) {
        !           194:         // stretch
        !           195:         glyphWidth = fitWidth = tileWidth;
        !           196:         glyphHeight = fitHeight = tileHeight;
        !           197:     } else if (processing == 'f') {
        !           198:         // fit
        !           199:         int hi = fitHeight * TILE_HEIGHT / (TILE_HEIGHT - 2 * padding);
        !           200:         int lo = max(1, min(fitHeight, round(1.2 * fitWidth * TILE_HEIGHT / TILE_WIDTH)));
        !           201:         glyphHeight = max(lo, min(hi, round((double)fitWidth * TILE_HEIGHT / TILE_WIDTH)));
        !           202:         glyphWidth = max(1, min(fitWidth, round(1.2 * glyphHeight * TILE_WIDTH / TILE_HEIGHT)));
        !           203:     } else {
        !           204:         // text
        !           205:         glyphWidth = max(1, min(fitWidth, round(1.4 * fitHeight * TILE_WIDTH / TILE_HEIGHT)));
        !           206:         glyphHeight = max(1, min(fitHeight, round(1.4 * fitWidth * TILE_HEIGHT / TILE_WIDTH)));
        !           207:     }
        !           208:
        !           209:     // map source pixels to target pixels...
        !           210:     int scaledX[TILE_WIDTH], scaledY[TILE_HEIGHT];
        !           211:     int stop0, stop1, stop2, stop3, stop4;
        !           212:     double map0, map1, map2, map3, map4;
        !           213:     int8_t *shifts;
        !           214:
        !           215:     // ... horizontally:
        !           216:
        !           217:     // horizontal coordinates on the source tile for the left border (stop0), 3 taps (stop1, 2, 3), and right border (stop4)
        !           218:     stop0 = 0;
        !           219:     stop1 = TILE_WIDTH / 5;   // 20%
        !           220:     stop2 = TILE_WIDTH / 2;   // 50%
        !           221:     stop3 = TILE_WIDTH * 4/5; // 80%
        !           222:     stop4 = TILE_WIDTH;
        !           223:
        !           224:     // corresponding coordinates on the target tile, taking into account centering and sub-pixel positioning
        !           225:     shifts = (glyphWidth > MAX_TILE_SIZE ? noShifts : tileShifts[row][column][0][glyphWidth - 1]);
        !           226:     map0 = (fitWidth - glyphWidth + (shifts[0] + shifts[1] < 0 ? 1 : 0)) / 2;
        !           227:     map1 = map0 + glyphWidth * (double)(stop1 - stop0) / (stop4 - stop0) + shifts[0] * 0.1;
        !           228:     map2 = map0 + glyphWidth * (double)(stop2 - stop0) / (stop4 - stop0) + shifts[2] * 0.1;
        !           229:     map3 = map0 + glyphWidth * (double)(stop3 - stop0) / (stop4 - stop0) + shifts[1] * 0.1;
        !           230:     map4 = map0 + glyphWidth;
        !           231:
        !           232:     // now we can interpolate the horizontal coordinates for all pixels
        !           233:     for (int x = stop0; x < stop1; x++) scaledX[x] = map0 + (map1 - map0) * (x - stop0) / (stop1 - stop0);
        !           234:     for (int x = stop1; x < stop2; x++) scaledX[x] = map1 + (map2 - map1) * (x - stop1) / (stop2 - stop1);
        !           235:     for (int x = stop2; x < stop3; x++) scaledX[x] = map2 + (map3 - map2) * (x - stop2) / (stop3 - stop2);
        !           236:     for (int x = stop3; x < stop4; x++) scaledX[x] = map3 + (map4 - map3) * (x - stop3) / (stop4 - stop3);
        !           237:
        !           238:     // ... vertically:
        !           239:
        !           240:     if (processing == 't') {
        !           241:         // vertical coordinates on the source tile for the top edge (stop0), stem (stop1), "x" (stop2, stop3), and bottom edge (stop4)
        !           242:         stop4 = TILE_HEIGHT;
        !           243:         stop3 = stop4 - TEXT_BASELINE;
        !           244:         stop2 = stop3 - TEXT_X_HEIGHT;
        !           245:         stop1 = stop2 / 3;
        !           246:         stop0 = 0;
        !           247:     } else {
        !           248:         // vertical coordinates on the source tile for the top edge (stop0), 3 taps (stop1, 2, 3), and bottom edge (stop4)
        !           249:         stop0 = 0;
        !           250:         stop1 = TILE_HEIGHT / 5;   // 20%
        !           251:         stop2 = TILE_HEIGHT / 2;   // 50%
        !           252:         stop3 = TILE_HEIGHT * 4/5; // 80%
        !           253:         stop4 = TILE_HEIGHT;
        !           254:     }
        !           255:
        !           256:     // corresponding coordinates on the target tile, taking into account centering
        !           257:     map0 = (fitHeight - glyphHeight) / 2;
        !           258:     map1 = map0 + glyphHeight * (double)(stop1 - stop0) / (stop4 - stop0);
        !           259:     map2 = map0 + glyphHeight * (double)(stop2 - stop0) / (stop4 - stop0);
        !           260:     map3 = map0 + glyphHeight * (double)(stop3 - stop0) / (stop4 - stop0);
        !           261:     map4 = map0 + glyphHeight;
        !           262:
        !           263:     // for text tiles, we must exactly align stops #2 and #3 with output pixels
        !           264:     if (processing == 't') {
        !           265:         map3 += round(map2) - map2;
        !           266:         map2 = round(map2);
        !           267:         map3 = max(map2 + 1, round(map3));
        !           268:         map1 = map0 + (map2 - map0) / 3;
        !           269:     }
        !           270:
        !           271:     // now add sub-pixel positioning (for text tiles, we have shifts[1] == shifts[2] == 0)
        !           272:     shifts = (glyphHeight > MAX_TILE_SIZE ? noShifts : tileShifts[row][column][1][glyphHeight - 1]);
        !           273:     map1 += shifts[0] * 0.1;
        !           274:     map2 += shifts[2] * 0.1;
        !           275:     map3 += shifts[1] * 0.1;
        !           276:
        !           277:     // finally we can interpolate the vertical coordinates for all pixels
        !           278:     for (int y = 0; y < stop0; y++) scaledY[y] = -1; // not mapped (can happen with fitted tiles)
        !           279:     for (int y = stop0; y < stop1; y++) scaledY[y] = map0 + (map1 - map0) * (y - stop0) / (stop1 - stop0);
        !           280:     for (int y = stop1; y < stop2; y++) scaledY[y] = map1 + (map2 - map1) * (y - stop1) / (stop2 - stop1);
        !           281:     for (int y = stop2; y < stop3; y++) scaledY[y] = map2 + (map3 - map2) * (y - stop2) / (stop3 - stop2);
        !           282:     for (int y = stop3; y < stop4; y++) scaledY[y] = map3 + (map4 - map3) * (y - stop3) / (stop4 - stop3);
        !           283:     for (int y = stop4; y < TILE_HEIGHT; y++) scaledY[y] = -1; // not mapped (can happen with fitted tiles)
        !           284:
        !           285:     // downscale source tile to accumulator
        !           286:     for (int y0 = 0; y0 < TILE_HEIGHT; y0++) {
        !           287:         int y1 = scaledY[y0];
        !           288:         if (y1 < 0 || y1 >= tileHeight) continue;
        !           289:         uint64_t *dst = &values[y1 * tileWidth];
        !           290:         Uint32 *src = TilesPNG->pixels; // each pixel is encoded as 0xffRRGGBB
        !           291:         src += (column * TILE_WIDTH) + (row * TILE_HEIGHT + y0) * PNG_WIDTH;
        !           292:         for (int x0 = 0; x0 < TILE_WIDTH; x0++) {
        !           293:             uint64_t value = src[x0] & 0xffU;
        !           294:             dst[scaledX[x0]] += (value * value) | 0x100000000U; // (gamma = 2.0, count = 1)
        !           295:         }
        !           296:         // interpolate skipped lines, if any
        !           297:         if (y1 >= 2 && scaledY[y0 - 1] == y1 - 2) {
        !           298:             for (int x1 = 0; x1 < tileWidth; x1++) {
        !           299:                 dst[x1 - tileWidth] = dst[x1 - 2*tileWidth] + dst[x1];
        !           300:             }
        !           301:         }
        !           302:     }
        !           303:     downscaled:
        !           304:
        !           305:     // procedural wall tops: diagonal sine waves
        !           306:     if ((row == 16 && column == 2 || row == 21 && column == 1 || row == 22 && column == 4) && !optimizing) {
        !           307:         for (int y = 0; y < tileHeight; y++) {
        !           308:             if (row != 21 && (y > tileHeight / 2 || (values[y * tileWidth] & 0xffffffffU))) break;
        !           309:             for (int x = 0; x < tileWidth; x++) {
        !           310:                 double value = sin(2. * PI * ((double)x / tileWidth * numHorizWaves
        !           311:                                             + (double)y / tileHeight * numVertWaves)) / 2. + 0.5;
        !           312:                 values[y * tileWidth + x] = (uint64_t)round(255 * 255 * value * value) | 0x100000000U;
        !           313:             }
        !           314:         }
        !           315:     }
        !           316:
        !           317:     // convert accumulator to image transparency
        !           318:     for (int y = 0; y < tileHeight; y++) {
        !           319:         Uint32 *pixel = surface->pixels; // each pixel is encoded as 0xAARRGGBB
        !           320:         pixel += (column * tileWidth) + (row * tileHeight + y) * surface->w;
        !           321:         for (int x = 0; x < tileWidth; x++) {
        !           322:             uint64_t value = values[y * tileWidth + x];
        !           323:
        !           324:             // average light intensity (linear scale, 0 .. 255*255)
        !           325:             value = ((value >> 32) ? (value & 0xffffffffU) / (value >> 32) : 0);
        !           326:
        !           327:             // metric for "blurriness": black (0) and white (255*255) pixels count for 0, gray pixels for 1
        !           328:             if (optimizing) blur += sin(PI/(255*255) * value);
        !           329:
        !           330:             // make text look less bold, at the cost of accuracy
        !           331:             if (processing == 't' || processing == '#') {
        !           332:                 value = (value < 255*255/2 ? value / 2 : value * 3/2 - 255*255/2);
        !           333:             }
        !           334:
        !           335:             // opacity (gamma-compressed, 0 .. 255)
        !           336:             uint32_t alpha = (value == 0 ? 0 : value > 64770 ? 255 : round(sqrt(value)));
        !           337:
        !           338:             *pixel++ = (alpha << 24) | 0xffffffU;
        !           339:         }
        !           340:     }
        !           341:
        !           342:     free(values);
        !           343:     return blur; // (used by the optimizer)
        !           344: }
        !           345:
        !           346:
        !           347: /// Finds the best possible sub-pixel alignments of tiles for their downscaling at every possible size.
        !           348: /// Results are recorded into `tileShifts`.
        !           349: ///
        !           350: /// This is a slow function (takes ~2 minutes) so the results are saved to disk and reloaded when Brogue starts.
        !           351: /// After you modify the PNG, you should also delete "tiles.bin" and run Brogue so that the new tiles get optimized.
        !           352: static void optimizeTiles() {
        !           353:     SDL_Window *window = SDL_CreateWindow("Brogue", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 400, 300, 0);
        !           354:
        !           355:     for (int row = 0; row < TILE_ROWS; row++) {
        !           356:         for (int column = 0; column < TILE_COLS; column++) {
        !           357:             if (tileEmpty[row][column]) continue;
        !           358:             char processing = TileProcessing[row][column];
        !           359:
        !           360:             // show what we are doing
        !           361:             char title[100];
        !           362:             sprintf(title, "Brogue - Optimizing tile %d / %d ...\n", row * TILE_COLS + column + 1, TILE_ROWS * TILE_COLS);
        !           363:             SDL_SetWindowTitle(window, title);
        !           364:             SDL_Surface *winSurface = SDL_GetWindowSurface(window);
        !           365:             if (!winSurface) sdlfatal(__FILE__, __LINE__);
        !           366:             if (SDL_BlitSurface(TilesPNG, &(SDL_Rect){.x=column*TILE_WIDTH, .y=row*TILE_HEIGHT, .w=TILE_WIDTH, .h=TILE_HEIGHT},
        !           367:                     winSurface, &(SDL_Rect){.x=0, .y=0, .w=TILE_WIDTH, .h=TILE_HEIGHT}) < 0) sdlfatal(__FILE__, __LINE__);
        !           368:             if (SDL_UpdateWindowSurface(window) < 0) sdlfatal(__FILE__, __LINE__);
        !           369:
        !           370:             // horizontal shifts
        !           371:             baseTileHeight = MAX_TILE_SIZE;
        !           372:             for (baseTileWidth = 5; baseTileWidth <= MAX_TILE_SIZE; baseTileWidth++) {
        !           373:                 int8_t *shifts = tileShifts[row][column][0][baseTileWidth - 1];
        !           374:                 SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, baseTileWidth * TILE_COLS, baseTileHeight * TILE_ROWS, 32, SDL_PIXELFORMAT_ARGB8888);
        !           375:                 if (!surface) sdlfatal(__FILE__, __LINE__);
        !           376:
        !           377:                 for (int i = 0; i < 3; i++) {
        !           378:                     for (int idx = 0; idx < (processing == 't' || processing == '#' ? 2 : 3); idx++) {
        !           379:                         double bestResult = 1e20;
        !           380:                         int8_t bestShift = 0;
        !           381:                         int8_t midShift = (idx == 2 ? (shifts[0] + shifts[1]) / 2 : 0);
        !           382:                         for (int8_t shift = midShift - 5; shift <= midShift + 5; shift++) {
        !           383:                             shifts[idx] = shift;
        !           384:                             if (processing == 't' || processing == '#') {
        !           385:                                 shifts[2] = (shifts[0] + shifts[1]) / 2;
        !           386:                             }
        !           387:                             double blur = downscaleTile(surface, baseTileWidth, baseTileHeight, row, column, true);
        !           388:                             if (blur < bestResult) {
        !           389:                                 bestResult = blur;
        !           390:                                 bestShift = shift;
        !           391:                             }
        !           392:                         }
        !           393:                         shifts[idx] = bestShift;
        !           394:                         if (processing == 't' || processing == '#') {
        !           395:                             shifts[2] = (shifts[0] + shifts[1]) / 2;
        !           396:                         }
        !           397:                     }
        !           398:                 }
        !           399:
        !           400:                 SDL_FreeSurface(surface);
        !           401:             }
        !           402:
        !           403:             // vertical shifts
        !           404:             baseTileWidth = MAX_TILE_SIZE;
        !           405:             for (baseTileHeight = 7; baseTileHeight <= MAX_TILE_SIZE; baseTileHeight++) {
        !           406:                 int8_t *shifts = tileShifts[row][column][1][baseTileHeight - 1];
        !           407:                 SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, baseTileWidth * TILE_COLS, baseTileHeight * TILE_ROWS, 32, SDL_PIXELFORMAT_ARGB8888);
        !           408:                 if (!surface) sdlfatal(__FILE__, __LINE__);
        !           409:
        !           410:                 for (int i = 0; i < 3; i++) {
        !           411:                     for (int idx = 0; idx < (processing == 't' ? 1 : 3); idx++) {
        !           412:                         double bestResult = 1e20;
        !           413:                         int8_t bestShift = 0;
        !           414:                         int8_t midShift = (idx == 2 ? (shifts[0] + shifts[1]) / 2 : 0);
        !           415:                         for (int8_t shift = midShift - 5; shift <= midShift + 5; shift++) {
        !           416:                             shifts[idx] = shift;
        !           417:                             double blur = downscaleTile(surface, baseTileWidth, baseTileHeight, row, column, true);
        !           418:                             if (blur < bestResult) {
        !           419:                                 bestResult = blur;
        !           420:                                 bestShift = shift;
        !           421:                             }
        !           422:                         }
        !           423:                         shifts[idx] = bestShift;
        !           424:                     }
        !           425:                 }
        !           426:
        !           427:                 SDL_FreeSurface(surface);
        !           428:             }
        !           429:         }
        !           430:     }
        !           431:     SDL_DestroyWindow(window);
        !           432: }
        !           433:
        !           434:
        !           435: /// Loads the PNG and analyses it.
        !           436: void initTiles() {
        !           437:
        !           438:     // load the large PNG
        !           439:     char filename[BROGUE_FILENAME_MAX];
        !           440:     sprintf(filename, "%s/assets/tiles.png", dataDirectory);
        !           441:     SDL_Surface *image = IMG_Load(filename);
        !           442:     if (!image) imgfatal(__FILE__, __LINE__);
        !           443:     TilesPNG = SDL_ConvertSurfaceFormat(image, SDL_PIXELFORMAT_ARGB8888, 0);
        !           444:     if (!TilesPNG) sdlfatal(__FILE__, __LINE__);
        !           445:     SDL_FreeSurface(image);
        !           446:
        !           447:     // measure padding
        !           448:     for (int row = 0; row < TILE_ROWS; row++) {
        !           449:         for (int column = 0; column < TILE_COLS; column++) {
        !           450:             tileEmpty[row][column] = isTileEmpty(row, column);
        !           451:             tilePadding[row][column] = (TileProcessing[row][column] == 'f' ? getPadding(row, column) : 0);
        !           452:         }
        !           453:     }
        !           454:
        !           455:     // load shifts
        !           456:     sprintf(filename, "%s/assets/tiles.bin", dataDirectory);
        !           457:     FILE *file = fopen(filename, "rb");
        !           458:     if (file) {
        !           459:         fread(tileShifts, 1, sizeof(tileShifts), file);
        !           460:         fclose(file);
        !           461:     } else {
        !           462:         optimizeTiles();
        !           463:         file = fopen(filename, "wb");
        !           464:         fwrite(tileShifts, 1, sizeof(tileShifts), file);
        !           465:         fclose(file);
        !           466:     }
        !           467: }
        !           468:
        !           469:
        !           470: /// Creates the textures to fit a specific output size
        !           471: /// (which is equal to the window size on standard DPI displays, but can be larger on HiDPI).
        !           472: ///
        !           473: /// We build up to 4 textures, with different tile sizes. Each texture has all the tiles on it.
        !           474: /// The first texture (`Textures[0]`) is always present, and is always the smallest of the bunch.
        !           475: ///
        !           476: /// The reason for having tiles of different sizes ready to be drawn on screen is that the window
        !           477: /// width is usually not a multiple of 100 (the `COLS` constant), and height not a multiple of 34
        !           478: /// (`ROWS`). Since tiles must have integer dimensions, that means some tiles must be larger by
        !           479: /// 1 pixel than others, so that we can cover the window without black padding on the sides nor
        !           480: /// columns/rows of blank pixels between tiles.
        !           481: ///
        !           482: /// If the window is so large that tiles would have to be over 64x64 pixels, we generate a single, large
        !           483: /// texture instead of four and use it for all tiles, allowing the renderer to do some linear interpolation.
        !           484: ///
        !           485: /// To ensure compatibility with older OpenGL drivers, texture dimensions are always powers of 2.
        !           486: ///
        !           487: /// \param outputWidth renderer's output width
        !           488: /// \param outputHeight renderer's output height
        !           489: ///
        !           490: static void createTextures(SDL_Renderer *renderer, int outputWidth, int outputHeight) {
        !           491:
        !           492:     // choose tile size
        !           493:     double tileAspectRatio = (double)(outputWidth * ROWS) / (outputHeight * COLS);
        !           494:     int newBaseTileWidth = max(1, outputWidth / COLS);
        !           495:     int newBaseTileHeight = max(1, outputHeight / ROWS);
        !           496:     if (newBaseTileWidth >= MAX_TILE_SIZE || newBaseTileHeight >= MAX_TILE_SIZE) {
        !           497:         newBaseTileWidth = max(1, min(TILE_WIDTH, round(TILE_HEIGHT * tileAspectRatio)));
        !           498:         newBaseTileHeight = max(1, min(TILE_HEIGHT, round(TILE_WIDTH / tileAspectRatio)));
        !           499:     }
        !           500:
        !           501:     // if tile size has not changed, we don't need to rebuild the tiles
        !           502:     if (baseTileWidth == newBaseTileWidth && baseTileHeight == newBaseTileHeight) {
        !           503:         return;
        !           504:     }
        !           505:
        !           506:     baseTileWidth = newBaseTileWidth;
        !           507:     baseTileHeight = newBaseTileHeight;
        !           508:
        !           509:     // destroy the old textures
        !           510:     for (int i = 0; i < 4; i++) {
        !           511:         if (Textures[i]) SDL_DestroyTexture(Textures[i]);
        !           512:         Textures[i] = NULL;
        !           513:     }
        !           514:
        !           515:     // choose the number of textures
        !           516:     if (baseTileWidth >= MAX_TILE_SIZE || baseTileHeight >= MAX_TILE_SIZE) {
        !           517:         numTextures = 1;
        !           518:         SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
        !           519:     } else {
        !           520:         numTextures = 4;
        !           521:         SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest");
        !           522:     }
        !           523:
        !           524:     // The original image will be resized to 4 possible sizes:
        !           525:     //  -  Textures[0]: tiles are   W   x   H   pixels
        !           526:     //  -  Textures[1]: tiles are (W+1) x   H   pixels
        !           527:     //  -  Textures[2]: tiles are   W   x (H+1) pixels
        !           528:     //  -  Textures[3]: tiles are (W+1) x (H+1) pixels
        !           529:
        !           530:     for (int i = 0; i < numTextures; i++) {
        !           531:
        !           532:         // choose dimensions
        !           533:         int tileWidth = baseTileWidth + (i == 1 || i == 3 ? 1 : 0);
        !           534:         int tileHeight = baseTileHeight + (i == 2 || i == 3 ? 1 : 0);
        !           535:         int surfaceWidth = 1, surfaceHeight = 1;
        !           536:         while (surfaceWidth < tileWidth * TILE_COLS) surfaceWidth *= 2;
        !           537:         while (surfaceHeight < tileHeight * TILE_ROWS) surfaceHeight *= 2;
        !           538:
        !           539:         // downscale the tiles
        !           540:         SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, surfaceWidth, surfaceHeight, 32, SDL_PIXELFORMAT_ARGB8888);
        !           541:         if (!surface) sdlfatal(__FILE__, __LINE__);
        !           542:         for (int row = 0; row < TILE_ROWS; row++) {
        !           543:             for (int column = 0; column < TILE_COLS; column++) {
        !           544:                 downscaleTile(surface, tileWidth, tileHeight, row, column, false);
        !           545:             }
        !           546:         }
        !           547:
        !           548:         // convert to texture
        !           549:         Textures[i] = SDL_CreateTextureFromSurface(renderer, surface);
        !           550:         if (!Textures[i]) sdlfatal(__FILE__, __LINE__);
        !           551:         if (SDL_SetTextureBlendMode(Textures[i], SDL_BLENDMODE_BLEND) < 0) sdlfatal(__FILE__, __LINE__);
        !           552:         SDL_FreeSurface(surface);
        !           553:     }
        !           554: }
        !           555:
        !           556:
        !           557: /// Updates the screen buffer.
        !           558: ///
        !           559: /// \param row row on screen (between 0 and ROWS-1)
        !           560: /// \param column column on screen (between 0 and COLS-1)
        !           561: /// \param charIndex glyph number (e.g 48 for "@", 281 for Dragon)
        !           562: /// \param foreRed red component of the foreground color (0..100)
        !           563: /// \param foreGreen green component of the foreground color (0..100)
        !           564: /// \param foreBlue blue component of the foreground color (0..100)
        !           565: /// \param backRed red component of the background color (0..100)
        !           566: /// \param backGreen green component of the background color (0..100)
        !           567: /// \param backBlue blue component of the background color (0..100)
        !           568: ///
        !           569: void updateTile(int row, int column, short charIndex,
        !           570:     short foreRed, short foreGreen, short foreBlue,
        !           571:     short backRed, short backGreen, short backBlue)
        !           572: {
        !           573:     screenTiles[row][column] = (ScreenTile){
        !           574:         .foreRed   = foreRed,
        !           575:         .foreGreen = foreGreen,
        !           576:         .foreBlue  = foreBlue,
        !           577:         .backRed   = backRed,
        !           578:         .backGreen = backGreen,
        !           579:         .backBlue  = backBlue,
        !           580:         .charIndex = charIndex,
        !           581:         .needsRefresh = 1
        !           582:     };
        !           583: }
        !           584:
        !           585:
        !           586: /// Draws everything on screen.
        !           587: ///
        !           588: /// OpenGL drivers don't like alternating between different textures too much, so we
        !           589: /// first draw the background colors then do 4 passes over the tiles, one pass per texture.
        !           590: ///
        !           591: /// Some video drivers are quite inefficient, notably in virtual machines, so
        !           592: /// there is a new `--no-gpu` command-line parameter to disable hardware acceleration.
        !           593: /// The software renderer does not support HiDPI, though.
        !           594: ///
        !           595: /// To improve performance of the software renderer, we don't redraw the whole screen but
        !           596: /// only the tiles that have changed recently (which is tracked with ScreenTile::needsRefresh).
        !           597: /// This works because, unlike the accelerated renderers, the software renderer draws on a
        !           598: /// single surface and doesn't do double-buffering.
        !           599: ///
        !           600: void updateScreen() {
        !           601:     if (!Win) return;
        !           602:
        !           603:     SDL_Renderer *renderer = SDL_GetRenderer(Win);
        !           604:     if (!renderer) {
        !           605:         renderer = SDL_CreateRenderer(Win, -1, (softwareRendering ? SDL_RENDERER_SOFTWARE : 0));
        !           606:         if (!renderer) sdlfatal(__FILE__, __LINE__);
        !           607:
        !           608:         if (SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) < 0) sdlfatal(__FILE__, __LINE__);
        !           609:
        !           610:         // see if we ended up using the software renderer or not
        !           611:         SDL_RendererInfo info;
        !           612:         if (SDL_GetRendererInfo(renderer, &info) < 0) sdlfatal(__FILE__, __LINE__);
        !           613:         softwareRendering = (strcmp(info.name, "software") == 0);
        !           614:     }
        !           615:
        !           616:     int outputWidth, outputHeight;
        !           617:     if (SDL_GetRendererOutputSize(renderer, &outputWidth, &outputHeight) < 0) sdlfatal(__FILE__, __LINE__);
        !           618:     if (outputWidth == 0 || outputHeight == 0) return;
        !           619:
        !           620:     createTextures(renderer, outputWidth, outputHeight);
        !           621:
        !           622:     if (!softwareRendering) {
        !           623:         // black out the frame (double-buffering invalidated it)
        !           624:         if (SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) < 0) sdlfatal(__FILE__, __LINE__);
        !           625:         if (SDL_RenderClear(renderer) < 0) sdlfatal(__FILE__, __LINE__);
        !           626:     }
        !           627:
        !           628:     // To please the OpenGL renderer, we'll proceed in 5 steps:
        !           629:     //  -1. background colors
        !           630:     //  0.  Textures[0]
        !           631:     //  1.  Textures[1]
        !           632:     //  2.  Textures[2]
        !           633:     //  3.  Textures[3]
        !           634:
        !           635:     for (int step = -1; step < numTextures; step++) {
        !           636:
        !           637:         for (int x = 0; x < COLS; x++) {
        !           638:             int tileWidth = ((x+1) * outputWidth / COLS) - (x * outputWidth / COLS);
        !           639:             if (tileWidth == 0) continue;
        !           640:
        !           641:             for (int y = 0; y < ROWS; y++) {
        !           642:                 int tileHeight = ((y+1) * outputHeight / ROWS) - (y * outputHeight / ROWS);
        !           643:                 if (tileHeight == 0) continue;
        !           644:
        !           645:                 ScreenTile *tile = &screenTiles[y][x];
        !           646:                 if (softwareRendering && !tile->needsRefresh) {
        !           647:                     continue; // software rendering does not use double-buffering, so the tile is still on screen
        !           648:                 }
        !           649:
        !           650:                 if (step < 0) {
        !           651:                     if (!softwareRendering && tile->backRed == 0 && tile->backGreen == 0 && tile->backBlue == 0) {
        !           652:                         continue; // SDL_RenderClear already painted everything black
        !           653:                     }
        !           654:
        !           655:                     SDL_Rect dest;
        !           656:                     dest.w = tileWidth;
        !           657:                     dest.h = tileHeight;
        !           658:                     dest.x = x * outputWidth / COLS;
        !           659:                     dest.y = y * outputHeight / ROWS;
        !           660:
        !           661:                     // paint the background
        !           662:                     if (SDL_SetRenderDrawColor(renderer,
        !           663:                         round(2.55 * tile->backRed),
        !           664:                         round(2.55 * tile->backGreen),
        !           665:                         round(2.55 * tile->backBlue), 255) < 0) sdlfatal(__FILE__, __LINE__);
        !           666:                     if (SDL_RenderFillRect(renderer, &dest) < 0) sdlfatal(__FILE__, __LINE__);
        !           667:
        !           668:                 } else {
        !           669:                     int textureIndex = (numTextures < 4 ? 0 : (tileWidth > baseTileWidth ? 1 : 0) + (tileHeight > baseTileHeight ? 2 : 0));
        !           670:                     if (step != textureIndex) {
        !           671:                         continue; // this tile uses another texture and gets painted at another step
        !           672:                     }
        !           673:
        !           674:                     int tileRow    = tile->charIndex / 16;
        !           675:                     int tileColumn = tile->charIndex % 16;
        !           676:
        !           677:                     if (tileEmpty[tileRow][tileColumn]
        !           678:                             && !(tileRow == 21 && tileColumn == 1)) {  // wall top (procedural)
        !           679:                         continue; // there is nothing to draw
        !           680:                     }
        !           681:
        !           682:                     SDL_Rect src;
        !           683:                     src.w = baseTileWidth  + (step == 1 || step == 3 ? 1 : 0);
        !           684:                     src.h = baseTileHeight + (step == 2 || step == 3 ? 1 : 0);
        !           685:                     src.x = src.w * tileColumn;
        !           686:                     src.y = src.h * tileRow;
        !           687:
        !           688:                     SDL_Rect dest;
        !           689:                     dest.w = tileWidth;
        !           690:                     dest.h = tileHeight;
        !           691:                     dest.x = x * outputWidth / COLS;
        !           692:                     dest.y = y * outputHeight / ROWS;
        !           693:
        !           694:                     // blend the foreground
        !           695:                     if (SDL_SetTextureColorMod(Textures[step],
        !           696:                         round(2.55 * tile->foreRed),
        !           697:                         round(2.55 * tile->foreGreen),
        !           698:                         round(2.55 * tile->foreBlue)) < 0) sdlfatal(__FILE__, __LINE__);
        !           699:                     if (SDL_RenderCopy(renderer, Textures[step], &src, &dest) < 0) sdlfatal(__FILE__, __LINE__);
        !           700:                 }
        !           701:             }
        !           702:         }
        !           703:     }
        !           704:
        !           705:     SDL_RenderPresent(renderer);
        !           706:
        !           707:     // the screen is now up to date
        !           708:     for (int y = 0; y < ROWS; y++) {
        !           709:         for (int x = 0; x < COLS; x++) {
        !           710:             screenTiles[y][x].needsRefresh = 0;
        !           711:         }
        !           712:     }
        !           713: }
        !           714:
        !           715:
        !           716: /*
        !           717: Creates or resizes the game window with the currently loaded font.
        !           718: */
        !           719: void resizeWindow(int width, int height) {
        !           720:
        !           721:     SDL_DisplayMode mode;
        !           722:     if (SDL_GetCurrentDisplayMode(0, &mode) < 0) sdlfatal(__FILE__, __LINE__);
        !           723:
        !           724:     // 70% of monitor width by default, with height following 16:10 aspect ratio
        !           725:     if (width < 0) width = mode.w * 7/10;
        !           726:     if (height < 0) height = width * 10/16;
        !           727:
        !           728:     // go to fullscreen mode if the window is as big as the screen
        !           729:     if (width >= mode.w && height >= mode.h) fullScreen = true;
        !           730:
        !           731:     if (Win == NULL) {
        !           732:         // create the window
        !           733:         Win = SDL_CreateWindow("Brogue",
        !           734:             SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height,
        !           735:             SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | (fullScreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0));
        !           736:         if (!Win) sdlfatal(__FILE__, __LINE__);
        !           737:
        !           738:         // set its icon
        !           739:         char filename[BROGUE_FILENAME_MAX];
        !           740:         sprintf(filename, "%s/assets/icon.png", dataDirectory);
        !           741:         SDL_Surface *icon = IMG_Load(filename);
        !           742:         if (!icon) imgfatal(__FILE__, __LINE__);
        !           743:         SDL_SetWindowIcon(Win, icon);
        !           744:         SDL_FreeSurface(icon);
        !           745:     }
        !           746:
        !           747:     if (fullScreen) {
        !           748:         if (!(SDL_GetWindowFlags(Win) & SDL_WINDOW_FULLSCREEN_DESKTOP)) {
        !           749:             // switch to fullscreen mode
        !           750:             if (SDL_SetWindowFullscreen(Win, SDL_WINDOW_FULLSCREEN_DESKTOP) < 0) sdlfatal(__FILE__, __LINE__);
        !           751:         }
        !           752:     } else {
        !           753:         if (SDL_GetWindowFlags(Win) & SDL_WINDOW_FULLSCREEN_DESKTOP) {
        !           754:             // switch to windowed mode
        !           755:             if (SDL_SetWindowFullscreen(Win, 0) < 0) sdlfatal(__FILE__, __LINE__);
        !           756:         } else {
        !           757:             // what is the current size?
        !           758:             SDL_GetWindowSize(Win, &windowWidth, &windowHeight);
        !           759:             if (windowWidth != width || windowHeight != height) {
        !           760:                 // resize the window
        !           761:                 SDL_SetWindowSize(Win, width, height);
        !           762:                 SDL_RestoreWindow(Win);
        !           763:             }
        !           764:         }
        !           765:     }
        !           766:
        !           767:     SDL_GetWindowSize(Win, &windowWidth, &windowHeight);
        !           768:     refreshScreen();
        !           769:     updateScreen();
        !           770: }
        !           771:
        !           772:
        !           773: SDL_Surface *captureScreen() {
        !           774:     if (!Win) return NULL;
        !           775:
        !           776:     // get the renderer
        !           777:     SDL_Renderer *renderer = SDL_GetRenderer(Win);
        !           778:     if (!renderer) return NULL;
        !           779:
        !           780:     // get its size
        !           781:     int outputWidth, outputHeight;
        !           782:     if (SDL_GetRendererOutputSize(renderer, &outputWidth, &outputHeight) < 0) sdlfatal(__FILE__, __LINE__);
        !           783:     if (outputWidth == 0 || outputHeight == 0) return NULL;
        !           784:
        !           785:     // take a screenshot
        !           786:     SDL_Surface *screenshot = SDL_CreateRGBSurfaceWithFormat(0, outputWidth, outputHeight, 32, SDL_PIXELFORMAT_ARGB8888);
        !           787:     if (!screenshot) sdlfatal(__FILE__, __LINE__);
        !           788:     if (SDL_RenderReadPixels(renderer, NULL, SDL_PIXELFORMAT_ARGB8888, screenshot->pixels, outputWidth * 4) < 0) sdlfatal(__FILE__, __LINE__);
        !           789:     return screenshot;
        !           790: }

CVSweb