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