#include #include #include #include "platform.h" #include "tiles.h" #define PI 3.14159265358979323846 #define PNG_WIDTH 2048 // width (px) of the source PNG #define PNG_HEIGHT 5568 // height (px) of the source PNG #define TILE_WIDTH 128 // width (px) of a single tile in the source PNG #define TILE_HEIGHT 232 // height (px) of a single tile in the source PNG #define TILE_ROWS 24 // number of rows in the source PNG #define TILE_COLS 16 // number of columns in the source PNG #define TEXT_X_HEIGHT 100 // height (px) of the 'x' outline #define TEXT_BASELINE 46 // height (px) of the blank space below the 'x' outline #define MAX_TILE_SIZE 64 // maximum width or height (px) of screen tiles before we switch to linear interpolation // How each tile should be processed: // - 's' = stretch: tile stretches to fill the space // - 'f' = fit: preserve aspect ratio (but tile can stretch up to 20%) // - 't' = text: characters must line up vertically (max. stretch 40%) // - '#' = symbols: other Unicode characters (max. stretch 40%) static const char TileProcessing[TILE_ROWS][TILE_COLS+1] = { "ffffffffffffffff", "ssssssssssssssss", "#t##########t#t#", "tttttttttttt###t", "#ttttttttttttttt", "ttttttttttt#####", "#ttttttttttttttt", "ttttttttttt#####", "################", "################", "################", "################", "tttttttttttttttt", "ttttttt#tttttttt", "tttttttttttttttt", "ttttttt#tttttttt", "ffsfsfsffsssssss", "ssfsfsffffffffff", "fffffffffffffsff", "ffffffffffffffff", "fsssfffffffffffs", "fsffffffffffffff", "ffffssssffssffff", "ffffsfffffssssff" }; typedef struct ScreenTile { short foreRed, foreGreen, foreBlue; // foreground color (0..100) short backRed, backGreen, backBlue; // background color (0..100) short charIndex; // index of the glyph to draw short needsRefresh; // true if the tile has changed since the last screen refresh, else false } ScreenTile; static SDL_Window *Win = NULL; // the SDL window static SDL_Surface *TilesPNG; // source PNG static SDL_Texture *Textures[4]; // textures used by the renderer to draw tiles static int numTextures = 0; // how many textures are available in `Textures` static int8_t tilePadding[TILE_ROWS][TILE_COLS]; // how many black lines are at the top/bottom of each tile in the source PNG static boolean tileEmpty[TILE_ROWS][TILE_COLS]; // true if a tile is completely black in the source PNG, else false // How much should a corner of a tile be shifted by before its downscaling, to improve its sharpness. // The first two dimensions are the tile's coordinates (row and column, both zero-based). // The third dimension is either 0 for shifts along the horizontal axis, or 1 for the vertical axis. // The fourth dimention is the downscaling operation's target size (width for horizontal axis, height for vertical). // The last dimension is 0 for top/left, 1 for bottom/right, 2 for center. // The values stored in tileShifts are signed integers. Unit is 1/10th of a pixel. static int8_t tileShifts[TILE_ROWS][TILE_COLS][2][MAX_TILE_SIZE][3]; static ScreenTile screenTiles[ROWS][COLS]; // buffer for the expected contents of the screen static int baseTileWidth = -1; // width (px) of tiles in the smallest texture (`Textures[0]`) static int baseTileHeight = -1; // height (px) of tiles in the smallest texture (`Textures[0]`) int windowWidth = -1; // the SDL window's width (in "screen units", not pixels) int windowHeight = -1; // the SDL window's height (in "screen units", not pixels) boolean fullScreen = false; // true if the window should be full-screen, else false boolean softwareRendering = false; // true if hardware acceleration is disabled (by choice or by force) /// Prints the fatal error message provided by SDL then closes the app. static void sdlfatal(char *file, int line) { fprintf(stderr, "Fatal SDL error (%s:%d): %s\n", file, line, SDL_GetError()); exit(1); } /// Prints the fatal error message provided by SDL_image then closes the app. static void imgfatal(char *file, int line) { fprintf(stderr, "Fatal SDL_image error (%s:%d): %s\n", file, line, IMG_GetError()); exit(1); } #if !SDL_VERSION_ATLEAST(2, 0, 5) SDL_Surface *SDL_CreateRGBSurfaceWithFormat(Uint32 flags, int width, int height, int depth, Uint32 format) { Uint32 r, g, b, a; if (!SDL_PixelFormatEnumToMasks(format, &depth, &r, &g, &b, &a)) sdlfatal(__FILE__, __LINE__); return SDL_CreateRGBSurface(flags, width, height, depth, r, g, b, a); } #endif /// Returns the numbers of black lines at the top and bottom of a given glyph in the source PNG. /// /// For example, if the glyph has 30 black lines at the top and 40 at the bottom, the function /// returns 30 (the least of the two). /// /// In case the glyph is very small, the function never returns more than TILE_HEIGHT / 4. /// This is to avoid drawing the glyph more than twice its size relative to other glyphs /// when the window's aspect ratio is very large (super wide). /// static int getPadding(int row, int column) { int padding; Uint32 *pixels = TilesPNG->pixels; // each pixel is encoded as 0xffRRGGBB for (padding = 0; padding < TILE_HEIGHT / 4; padding++) { for (int x = 0; x < TILE_WIDTH; x++) { int y1 = padding; int y2 = TILE_HEIGHT - padding - 1; if (pixels[(x + column * TILE_WIDTH) + (y1 + row * TILE_HEIGHT) * PNG_WIDTH] & 0xffffffU || pixels[(x + column * TILE_WIDTH) + (y2 + row * TILE_HEIGHT) * PNG_WIDTH] & 0xffffffU) { return padding; } } } return padding; } /// Tells if a tile is completely empty (black) in the source PNG. static boolean isTileEmpty(int row, int column) { Uint32 *pixels = TilesPNG->pixels; // each pixel is encoded as 0xffRRGGBB for (int y = 0; y < TILE_HEIGHT; y++) { for (int x = 0; x < TILE_WIDTH; x++) { if (pixels[(x + column * TILE_WIDTH) + (y + row * TILE_HEIGHT) * PNG_WIDTH] & 0xffffffU) { return false; } } } return true; } /// Downscales a tile to the specified size. /// /// The downscaling is performed in linear color space, rather than in gamma-compressed space which would cause /// dimming (average of a black pixel and a white pixel should be 50% grey = 0.5 luminance = 187 sRGB, not 128). /// To simplify the computation, we assume a gamma of 2.0. /// /// The tile is logically split into 16 parts (sliced by 3 vertical lines and 3 horizontal ones), so that: /// /// - the four corners can be positioned to preserve the tile's aspect ratio, /// possibly filling the blank spaces at the top and bottom; /// - the top, bottom, left, right, and center areas can be independently aligned /// with output pixels to improve sharpness. This is achieved by adding pre-computed /// sub-pixel shifts to the target coordinates as we map source pixels to target pixels. /// /// Some tiles, like walls and doors, always stretch to fill the space regardless of aspect ratio. /// Other tiles must preserve aspect ratio, but we allow up to 20% stretch for graphic tiles and /// 40% for text tiles. /// /// For text tiles (letters, digits, punctuation signs), the characters' x-height and baseline /// (corresponding to the top and bottom of "x") are pixel-aligned regardless of outlines. /// This ensures that all letters neatly line up without jumping up and down. /// We also reduce perceived boldness by applying a custom brightness curve; /// it makes the text more legible at the smaller sizes. /// /// Wall tops are diagonal sine waves, approximately 4 pixels apart. /// /// \param surface the target surface /// \param tileWidth width (px) of tiles in the target surface /// \param tileHeight height (px) of tiles in the target surface /// \param row row (zero-based) on which the tile is located in both the source PNG and the target surface /// \param column column (zero-based) in which the tile is located in both the source PNG and the target surface /// \param optimizing pass true when optimizing tiles, else false /// \return estimated amount of blur in the resulting tile (when optimizing) /// static double downscaleTile(SDL_Surface *surface, int tileWidth, int tileHeight, int row, int column, boolean optimizing) { int8_t noShifts[3] = {0, 0, 0}; int padding = tilePadding[row][column]; // how much blank spaces there is at the top and bottom of the source tile char processing = TileProcessing[row][column]; // how should this tile be processed? // Size of the area the glyph must fit into int fitWidth = max(1, baseTileWidth); int fitHeight = max(1, baseTileHeight); // Number of sine waves that can fit in the tile (for wall tops) const int numHorizWaves = max(2, min(6, round(fitWidth * .25))); const int numVertWaves = max(2, min(11, round(fitHeight * .25))); // Size of the downscaled glyph int glyphWidth, glyphHeight; // accumulator for pixel values (linear color space), encoded as // 0xCCCCCCCCSSSSSSSS where C is a counter and S is a sum of squares uint64_t *values = malloc(tileWidth * tileHeight * sizeof(uint64_t)); memset(values, 0, tileWidth * tileHeight * sizeof(uint64_t)); double blur = 0; // if the tile is empty, we can skip the downscaling if (tileEmpty[row][column]) goto downscaled; // decide how large we can draw the glyph if (processing == 's' || optimizing) { // stretch glyphWidth = fitWidth = tileWidth; glyphHeight = fitHeight = tileHeight; } else if (processing == 'f') { // fit int hi = fitHeight * TILE_HEIGHT / (TILE_HEIGHT - 2 * padding); int lo = max(1, min(fitHeight, round(1.2 * fitWidth * TILE_HEIGHT / TILE_WIDTH))); glyphHeight = max(lo, min(hi, round((double)fitWidth * TILE_HEIGHT / TILE_WIDTH))); glyphWidth = max(1, min(fitWidth, round(1.2 * glyphHeight * TILE_WIDTH / TILE_HEIGHT))); } else { // text glyphWidth = max(1, min(fitWidth, round(1.4 * fitHeight * TILE_WIDTH / TILE_HEIGHT))); glyphHeight = max(1, min(fitHeight, round(1.4 * fitWidth * TILE_HEIGHT / TILE_WIDTH))); } // map source pixels to target pixels... int scaledX[TILE_WIDTH], scaledY[TILE_HEIGHT]; int stop0, stop1, stop2, stop3, stop4; double map0, map1, map2, map3, map4; int8_t *shifts; // ... horizontally: // horizontal coordinates on the source tile for the left border (stop0), 3 taps (stop1, 2, 3), and right border (stop4) stop0 = 0; stop1 = TILE_WIDTH / 5; // 20% stop2 = TILE_WIDTH / 2; // 50% stop3 = TILE_WIDTH * 4/5; // 80% stop4 = TILE_WIDTH; // corresponding coordinates on the target tile, taking into account centering and sub-pixel positioning shifts = (glyphWidth > MAX_TILE_SIZE ? noShifts : tileShifts[row][column][0][glyphWidth - 1]); map0 = (fitWidth - glyphWidth + (shifts[0] + shifts[1] < 0 ? 1 : 0)) / 2; map1 = map0 + glyphWidth * (double)(stop1 - stop0) / (stop4 - stop0) + shifts[0] * 0.1; map2 = map0 + glyphWidth * (double)(stop2 - stop0) / (stop4 - stop0) + shifts[2] * 0.1; map3 = map0 + glyphWidth * (double)(stop3 - stop0) / (stop4 - stop0) + shifts[1] * 0.1; map4 = map0 + glyphWidth; // now we can interpolate the horizontal coordinates for all pixels for (int x = stop0; x < stop1; x++) scaledX[x] = map0 + (map1 - map0) * (x - stop0) / (stop1 - stop0); for (int x = stop1; x < stop2; x++) scaledX[x] = map1 + (map2 - map1) * (x - stop1) / (stop2 - stop1); for (int x = stop2; x < stop3; x++) scaledX[x] = map2 + (map3 - map2) * (x - stop2) / (stop3 - stop2); for (int x = stop3; x < stop4; x++) scaledX[x] = map3 + (map4 - map3) * (x - stop3) / (stop4 - stop3); // ... vertically: if (processing == 't') { // vertical coordinates on the source tile for the top edge (stop0), stem (stop1), "x" (stop2, stop3), and bottom edge (stop4) stop4 = TILE_HEIGHT; stop3 = stop4 - TEXT_BASELINE; stop2 = stop3 - TEXT_X_HEIGHT; stop1 = stop2 / 3; stop0 = 0; } else { // vertical coordinates on the source tile for the top edge (stop0), 3 taps (stop1, 2, 3), and bottom edge (stop4) stop0 = 0; stop1 = TILE_HEIGHT / 5; // 20% stop2 = TILE_HEIGHT / 2; // 50% stop3 = TILE_HEIGHT * 4/5; // 80% stop4 = TILE_HEIGHT; } // corresponding coordinates on the target tile, taking into account centering map0 = (fitHeight - glyphHeight) / 2; map1 = map0 + glyphHeight * (double)(stop1 - stop0) / (stop4 - stop0); map2 = map0 + glyphHeight * (double)(stop2 - stop0) / (stop4 - stop0); map3 = map0 + glyphHeight * (double)(stop3 - stop0) / (stop4 - stop0); map4 = map0 + glyphHeight; // for text tiles, we must exactly align stops #2 and #3 with output pixels if (processing == 't') { map3 += round(map2) - map2; map2 = round(map2); map3 = max(map2 + 1, round(map3)); map1 = map0 + (map2 - map0) / 3; } // now add sub-pixel positioning (for text tiles, we have shifts[1] == shifts[2] == 0) shifts = (glyphHeight > MAX_TILE_SIZE ? noShifts : tileShifts[row][column][1][glyphHeight - 1]); map1 += shifts[0] * 0.1; map2 += shifts[2] * 0.1; map3 += shifts[1] * 0.1; // finally we can interpolate the vertical coordinates for all pixels for (int y = 0; y < stop0; y++) scaledY[y] = -1; // not mapped (can happen with fitted tiles) for (int y = stop0; y < stop1; y++) scaledY[y] = map0 + (map1 - map0) * (y - stop0) / (stop1 - stop0); for (int y = stop1; y < stop2; y++) scaledY[y] = map1 + (map2 - map1) * (y - stop1) / (stop2 - stop1); for (int y = stop2; y < stop3; y++) scaledY[y] = map2 + (map3 - map2) * (y - stop2) / (stop3 - stop2); for (int y = stop3; y < stop4; y++) scaledY[y] = map3 + (map4 - map3) * (y - stop3) / (stop4 - stop3); for (int y = stop4; y < TILE_HEIGHT; y++) scaledY[y] = -1; // not mapped (can happen with fitted tiles) // downscale source tile to accumulator for (int y0 = 0; y0 < TILE_HEIGHT; y0++) { int y1 = scaledY[y0]; if (y1 < 0 || y1 >= tileHeight) continue; uint64_t *dst = &values[y1 * tileWidth]; Uint32 *src = TilesPNG->pixels; // each pixel is encoded as 0xffRRGGBB src += (column * TILE_WIDTH) + (row * TILE_HEIGHT + y0) * PNG_WIDTH; for (int x0 = 0; x0 < TILE_WIDTH; x0++) { uint64_t value = src[x0] & 0xffU; dst[scaledX[x0]] += (value * value) | 0x100000000U; // (gamma = 2.0, count = 1) } // interpolate skipped lines, if any if (y1 >= 2 && scaledY[y0 - 1] == y1 - 2) { for (int x1 = 0; x1 < tileWidth; x1++) { dst[x1 - tileWidth] = dst[x1 - 2*tileWidth] + dst[x1]; } } } downscaled: // procedural wall tops: diagonal sine waves if ((row == 16 && column == 2 || row == 21 && column == 1 || row == 22 && column == 4) && !optimizing) { for (int y = 0; y < tileHeight; y++) { if (row != 21 && (y > tileHeight / 2 || (values[y * tileWidth] & 0xffffffffU))) break; for (int x = 0; x < tileWidth; x++) { double value = sin(2. * PI * ((double)x / tileWidth * numHorizWaves + (double)y / tileHeight * numVertWaves)) / 2. + 0.5; values[y * tileWidth + x] = (uint64_t)round(255 * 255 * value * value) | 0x100000000U; } } } // convert accumulator to image transparency for (int y = 0; y < tileHeight; y++) { Uint32 *pixel = surface->pixels; // each pixel is encoded as 0xAARRGGBB pixel += (column * tileWidth) + (row * tileHeight + y) * surface->w; for (int x = 0; x < tileWidth; x++) { uint64_t value = values[y * tileWidth + x]; // average light intensity (linear scale, 0 .. 255*255) value = ((value >> 32) ? (value & 0xffffffffU) / (value >> 32) : 0); // metric for "blurriness": black (0) and white (255*255) pixels count for 0, gray pixels for 1 if (optimizing) blur += sin(PI/(255*255) * value); // make text look less bold, at the cost of accuracy if (processing == 't' || processing == '#') { value = (value < 255*255/2 ? value / 2 : value * 3/2 - 255*255/2); } // opacity (gamma-compressed, 0 .. 255) uint32_t alpha = (value == 0 ? 0 : value > 64770 ? 255 : round(sqrt(value))); *pixel++ = (alpha << 24) | 0xffffffU; } } free(values); return blur; // (used by the optimizer) } /// Finds the best possible sub-pixel alignments of tiles for their downscaling at every possible size. /// Results are recorded into `tileShifts`. /// /// This is a slow function (takes ~2 minutes) so the results are saved to disk and reloaded when Brogue starts. /// After you modify the PNG, you should also delete "tiles.bin" and run Brogue so that the new tiles get optimized. static void optimizeTiles() { SDL_Window *window = SDL_CreateWindow("Brogue", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 400, 300, 0); for (int row = 0; row < TILE_ROWS; row++) { for (int column = 0; column < TILE_COLS; column++) { if (tileEmpty[row][column]) continue; char processing = TileProcessing[row][column]; // show what we are doing char title[100]; sprintf(title, "Brogue - Optimizing tile %d / %d ...\n", row * TILE_COLS + column + 1, TILE_ROWS * TILE_COLS); SDL_SetWindowTitle(window, title); SDL_Surface *winSurface = SDL_GetWindowSurface(window); if (!winSurface) sdlfatal(__FILE__, __LINE__); if (SDL_BlitSurface(TilesPNG, &(SDL_Rect){.x=column*TILE_WIDTH, .y=row*TILE_HEIGHT, .w=TILE_WIDTH, .h=TILE_HEIGHT}, winSurface, &(SDL_Rect){.x=0, .y=0, .w=TILE_WIDTH, .h=TILE_HEIGHT}) < 0) sdlfatal(__FILE__, __LINE__); if (SDL_UpdateWindowSurface(window) < 0) sdlfatal(__FILE__, __LINE__); // horizontal shifts baseTileHeight = MAX_TILE_SIZE; for (baseTileWidth = 5; baseTileWidth <= MAX_TILE_SIZE; baseTileWidth++) { int8_t *shifts = tileShifts[row][column][0][baseTileWidth - 1]; SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, baseTileWidth * TILE_COLS, baseTileHeight * TILE_ROWS, 32, SDL_PIXELFORMAT_ARGB8888); if (!surface) sdlfatal(__FILE__, __LINE__); for (int i = 0; i < 3; i++) { for (int idx = 0; idx < (processing == 't' || processing == '#' ? 2 : 3); idx++) { double bestResult = 1e20; int8_t bestShift = 0; int8_t midShift = (idx == 2 ? (shifts[0] + shifts[1]) / 2 : 0); for (int8_t shift = midShift - 5; shift <= midShift + 5; shift++) { shifts[idx] = shift; if (processing == 't' || processing == '#') { shifts[2] = (shifts[0] + shifts[1]) / 2; } double blur = downscaleTile(surface, baseTileWidth, baseTileHeight, row, column, true); if (blur < bestResult) { bestResult = blur; bestShift = shift; } } shifts[idx] = bestShift; if (processing == 't' || processing == '#') { shifts[2] = (shifts[0] + shifts[1]) / 2; } } } SDL_FreeSurface(surface); } // vertical shifts baseTileWidth = MAX_TILE_SIZE; for (baseTileHeight = 7; baseTileHeight <= MAX_TILE_SIZE; baseTileHeight++) { int8_t *shifts = tileShifts[row][column][1][baseTileHeight - 1]; SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, baseTileWidth * TILE_COLS, baseTileHeight * TILE_ROWS, 32, SDL_PIXELFORMAT_ARGB8888); if (!surface) sdlfatal(__FILE__, __LINE__); for (int i = 0; i < 3; i++) { for (int idx = 0; idx < (processing == 't' ? 1 : 3); idx++) { double bestResult = 1e20; int8_t bestShift = 0; int8_t midShift = (idx == 2 ? (shifts[0] + shifts[1]) / 2 : 0); for (int8_t shift = midShift - 5; shift <= midShift + 5; shift++) { shifts[idx] = shift; double blur = downscaleTile(surface, baseTileWidth, baseTileHeight, row, column, true); if (blur < bestResult) { bestResult = blur; bestShift = shift; } } shifts[idx] = bestShift; } } SDL_FreeSurface(surface); } } } SDL_DestroyWindow(window); } /// Loads the PNG and analyses it. void initTiles() { // load the large PNG char filename[BROGUE_FILENAME_MAX]; sprintf(filename, "%s/assets/tiles.png", dataDirectory); SDL_Surface *image = IMG_Load(filename); if (!image) imgfatal(__FILE__, __LINE__); TilesPNG = SDL_ConvertSurfaceFormat(image, SDL_PIXELFORMAT_ARGB8888, 0); if (!TilesPNG) sdlfatal(__FILE__, __LINE__); SDL_FreeSurface(image); // measure padding for (int row = 0; row < TILE_ROWS; row++) { for (int column = 0; column < TILE_COLS; column++) { tileEmpty[row][column] = isTileEmpty(row, column); tilePadding[row][column] = (TileProcessing[row][column] == 'f' ? getPadding(row, column) : 0); } } // load shifts sprintf(filename, "%s/assets/tiles.bin", dataDirectory); FILE *file = fopen(filename, "rb"); if (file) { fread(tileShifts, 1, sizeof(tileShifts), file); fclose(file); } else { optimizeTiles(); file = fopen(filename, "wb"); fwrite(tileShifts, 1, sizeof(tileShifts), file); fclose(file); } } /// Creates the textures to fit a specific output size /// (which is equal to the window size on standard DPI displays, but can be larger on HiDPI). /// /// We build up to 4 textures, with different tile sizes. Each texture has all the tiles on it. /// The first texture (`Textures[0]`) is always present, and is always the smallest of the bunch. /// /// The reason for having tiles of different sizes ready to be drawn on screen is that the window /// width is usually not a multiple of 100 (the `COLS` constant), and height not a multiple of 34 /// (`ROWS`). Since tiles must have integer dimensions, that means some tiles must be larger by /// 1 pixel than others, so that we can cover the window without black padding on the sides nor /// columns/rows of blank pixels between tiles. /// /// If the window is so large that tiles would have to be over 64x64 pixels, we generate a single, large /// texture instead of four and use it for all tiles, allowing the renderer to do some linear interpolation. /// /// To ensure compatibility with older OpenGL drivers, texture dimensions are always powers of 2. /// /// \param outputWidth renderer's output width /// \param outputHeight renderer's output height /// static void createTextures(SDL_Renderer *renderer, int outputWidth, int outputHeight) { // choose tile size double tileAspectRatio = (double)(outputWidth * ROWS) / (outputHeight * COLS); int newBaseTileWidth = max(1, outputWidth / COLS); int newBaseTileHeight = max(1, outputHeight / ROWS); if (newBaseTileWidth >= MAX_TILE_SIZE || newBaseTileHeight >= MAX_TILE_SIZE) { newBaseTileWidth = max(1, min(TILE_WIDTH, round(TILE_HEIGHT * tileAspectRatio))); newBaseTileHeight = max(1, min(TILE_HEIGHT, round(TILE_WIDTH / tileAspectRatio))); } // if tile size has not changed, we don't need to rebuild the tiles if (baseTileWidth == newBaseTileWidth && baseTileHeight == newBaseTileHeight) { return; } baseTileWidth = newBaseTileWidth; baseTileHeight = newBaseTileHeight; // destroy the old textures for (int i = 0; i < 4; i++) { if (Textures[i]) SDL_DestroyTexture(Textures[i]); Textures[i] = NULL; } // choose the number of textures if (baseTileWidth >= MAX_TILE_SIZE || baseTileHeight >= MAX_TILE_SIZE) { numTextures = 1; SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); } else { numTextures = 4; SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest"); } // The original image will be resized to 4 possible sizes: // - Textures[0]: tiles are W x H pixels // - Textures[1]: tiles are (W+1) x H pixels // - Textures[2]: tiles are W x (H+1) pixels // - Textures[3]: tiles are (W+1) x (H+1) pixels for (int i = 0; i < numTextures; i++) { // choose dimensions int tileWidth = baseTileWidth + (i == 1 || i == 3 ? 1 : 0); int tileHeight = baseTileHeight + (i == 2 || i == 3 ? 1 : 0); int surfaceWidth = 1, surfaceHeight = 1; while (surfaceWidth < tileWidth * TILE_COLS) surfaceWidth *= 2; while (surfaceHeight < tileHeight * TILE_ROWS) surfaceHeight *= 2; // downscale the tiles SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, surfaceWidth, surfaceHeight, 32, SDL_PIXELFORMAT_ARGB8888); if (!surface) sdlfatal(__FILE__, __LINE__); for (int row = 0; row < TILE_ROWS; row++) { for (int column = 0; column < TILE_COLS; column++) { downscaleTile(surface, tileWidth, tileHeight, row, column, false); } } // convert to texture Textures[i] = SDL_CreateTextureFromSurface(renderer, surface); if (!Textures[i]) sdlfatal(__FILE__, __LINE__); if (SDL_SetTextureBlendMode(Textures[i], SDL_BLENDMODE_BLEND) < 0) sdlfatal(__FILE__, __LINE__); SDL_FreeSurface(surface); } } /// Updates the screen buffer. /// /// \param row row on screen (between 0 and ROWS-1) /// \param column column on screen (between 0 and COLS-1) /// \param charIndex glyph number (e.g 48 for "@", 281 for Dragon) /// \param foreRed red component of the foreground color (0..100) /// \param foreGreen green component of the foreground color (0..100) /// \param foreBlue blue component of the foreground color (0..100) /// \param backRed red component of the background color (0..100) /// \param backGreen green component of the background color (0..100) /// \param backBlue blue component of the background color (0..100) /// void updateTile(int row, int column, short charIndex, short foreRed, short foreGreen, short foreBlue, short backRed, short backGreen, short backBlue) { screenTiles[row][column] = (ScreenTile){ .foreRed = foreRed, .foreGreen = foreGreen, .foreBlue = foreBlue, .backRed = backRed, .backGreen = backGreen, .backBlue = backBlue, .charIndex = charIndex, .needsRefresh = 1 }; } /// Draws everything on screen. /// /// OpenGL drivers don't like alternating between different textures too much, so we /// first draw the background colors then do 4 passes over the tiles, one pass per texture. /// /// Some video drivers are quite inefficient, notably in virtual machines, so /// there is a new `--no-gpu` command-line parameter to disable hardware acceleration. /// The software renderer does not support HiDPI, though. /// /// To improve performance of the software renderer, we don't redraw the whole screen but /// only the tiles that have changed recently (which is tracked with ScreenTile::needsRefresh). /// This works because, unlike the accelerated renderers, the software renderer draws on a /// single surface and doesn't do double-buffering. /// void updateScreen() { if (!Win) return; SDL_Renderer *renderer = SDL_GetRenderer(Win); if (!renderer) { renderer = SDL_CreateRenderer(Win, -1, (softwareRendering ? SDL_RENDERER_SOFTWARE : 0)); if (!renderer) sdlfatal(__FILE__, __LINE__); if (SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) < 0) sdlfatal(__FILE__, __LINE__); // see if we ended up using the software renderer or not SDL_RendererInfo info; if (SDL_GetRendererInfo(renderer, &info) < 0) sdlfatal(__FILE__, __LINE__); softwareRendering = (strcmp(info.name, "software") == 0); } int outputWidth, outputHeight; if (SDL_GetRendererOutputSize(renderer, &outputWidth, &outputHeight) < 0) sdlfatal(__FILE__, __LINE__); if (outputWidth == 0 || outputHeight == 0) return; createTextures(renderer, outputWidth, outputHeight); if (!softwareRendering) { // black out the frame (double-buffering invalidated it) if (SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) < 0) sdlfatal(__FILE__, __LINE__); if (SDL_RenderClear(renderer) < 0) sdlfatal(__FILE__, __LINE__); } // To please the OpenGL renderer, we'll proceed in 5 steps: // -1. background colors // 0. Textures[0] // 1. Textures[1] // 2. Textures[2] // 3. Textures[3] for (int step = -1; step < numTextures; step++) { for (int x = 0; x < COLS; x++) { int tileWidth = ((x+1) * outputWidth / COLS) - (x * outputWidth / COLS); if (tileWidth == 0) continue; for (int y = 0; y < ROWS; y++) { int tileHeight = ((y+1) * outputHeight / ROWS) - (y * outputHeight / ROWS); if (tileHeight == 0) continue; ScreenTile *tile = &screenTiles[y][x]; if (softwareRendering && !tile->needsRefresh) { continue; // software rendering does not use double-buffering, so the tile is still on screen } if (step < 0) { if (!softwareRendering && tile->backRed == 0 && tile->backGreen == 0 && tile->backBlue == 0) { continue; // SDL_RenderClear already painted everything black } SDL_Rect dest; dest.w = tileWidth; dest.h = tileHeight; dest.x = x * outputWidth / COLS; dest.y = y * outputHeight / ROWS; // paint the background if (SDL_SetRenderDrawColor(renderer, round(2.55 * tile->backRed), round(2.55 * tile->backGreen), round(2.55 * tile->backBlue), 255) < 0) sdlfatal(__FILE__, __LINE__); if (SDL_RenderFillRect(renderer, &dest) < 0) sdlfatal(__FILE__, __LINE__); } else { int textureIndex = (numTextures < 4 ? 0 : (tileWidth > baseTileWidth ? 1 : 0) + (tileHeight > baseTileHeight ? 2 : 0)); if (step != textureIndex) { continue; // this tile uses another texture and gets painted at another step } int tileRow = tile->charIndex / 16; int tileColumn = tile->charIndex % 16; if (tileEmpty[tileRow][tileColumn] && !(tileRow == 21 && tileColumn == 1)) { // wall top (procedural) continue; // there is nothing to draw } SDL_Rect src; src.w = baseTileWidth + (step == 1 || step == 3 ? 1 : 0); src.h = baseTileHeight + (step == 2 || step == 3 ? 1 : 0); src.x = src.w * tileColumn; src.y = src.h * tileRow; SDL_Rect dest; dest.w = tileWidth; dest.h = tileHeight; dest.x = x * outputWidth / COLS; dest.y = y * outputHeight / ROWS; // blend the foreground if (SDL_SetTextureColorMod(Textures[step], round(2.55 * tile->foreRed), round(2.55 * tile->foreGreen), round(2.55 * tile->foreBlue)) < 0) sdlfatal(__FILE__, __LINE__); if (SDL_RenderCopy(renderer, Textures[step], &src, &dest) < 0) sdlfatal(__FILE__, __LINE__); } } } } SDL_RenderPresent(renderer); // the screen is now up to date for (int y = 0; y < ROWS; y++) { for (int x = 0; x < COLS; x++) { screenTiles[y][x].needsRefresh = 0; } } } /* Creates or resizes the game window with the currently loaded font. */ void resizeWindow(int width, int height) { SDL_DisplayMode mode; if (SDL_GetCurrentDisplayMode(0, &mode) < 0) sdlfatal(__FILE__, __LINE__); // 70% of monitor width by default, with height following 16:10 aspect ratio if (width < 0) width = mode.w * 7/10; if (height < 0) height = width * 10/16; // go to fullscreen mode if the window is as big as the screen if (width >= mode.w && height >= mode.h) fullScreen = true; if (Win == NULL) { // create the window Win = SDL_CreateWindow("Brogue", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | (fullScreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0)); if (!Win) sdlfatal(__FILE__, __LINE__); // set its icon char filename[BROGUE_FILENAME_MAX]; sprintf(filename, "%s/assets/icon.png", dataDirectory); SDL_Surface *icon = IMG_Load(filename); if (!icon) imgfatal(__FILE__, __LINE__); SDL_SetWindowIcon(Win, icon); SDL_FreeSurface(icon); } if (fullScreen) { if (!(SDL_GetWindowFlags(Win) & SDL_WINDOW_FULLSCREEN_DESKTOP)) { // switch to fullscreen mode if (SDL_SetWindowFullscreen(Win, SDL_WINDOW_FULLSCREEN_DESKTOP) < 0) sdlfatal(__FILE__, __LINE__); } } else { if (SDL_GetWindowFlags(Win) & SDL_WINDOW_FULLSCREEN_DESKTOP) { // switch to windowed mode if (SDL_SetWindowFullscreen(Win, 0) < 0) sdlfatal(__FILE__, __LINE__); } else { // what is the current size? SDL_GetWindowSize(Win, &windowWidth, &windowHeight); if (windowWidth != width || windowHeight != height) { // resize the window SDL_SetWindowSize(Win, width, height); SDL_RestoreWindow(Win); } } } SDL_GetWindowSize(Win, &windowWidth, &windowHeight); refreshScreen(); updateScreen(); } SDL_Surface *captureScreen() { if (!Win) return NULL; // get the renderer SDL_Renderer *renderer = SDL_GetRenderer(Win); if (!renderer) return NULL; // get its size int outputWidth, outputHeight; if (SDL_GetRendererOutputSize(renderer, &outputWidth, &outputHeight) < 0) sdlfatal(__FILE__, __LINE__); if (outputWidth == 0 || outputHeight == 0) return NULL; // take a screenshot SDL_Surface *screenshot = SDL_CreateRGBSurfaceWithFormat(0, outputWidth, outputHeight, 32, SDL_PIXELFORMAT_ARGB8888); if (!screenshot) sdlfatal(__FILE__, __LINE__); if (SDL_RenderReadPixels(renderer, NULL, SDL_PIXELFORMAT_ARGB8888, screenshot->pixels, outputWidth * 4) < 0) sdlfatal(__FILE__, __LINE__); return screenshot; }