[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

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