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