openMSX
SDLImage.cc
Go to the documentation of this file.
1 #include "SDLImage.hh"
2 #include "PNG.hh"
3 #include "SDLOutputSurface.hh"
4 #include "checked_cast.hh"
5 #include "ranges.hh"
6 #include "xrange.hh"
7 #include <cassert>
8 #include <cstdlib>
9 #include <cmath>
10 #include <SDL.h>
11 
12 using namespace gl;
13 
14 namespace openmsx {
15 
16 static SDLSurfacePtr getTempSurface(ivec2 size_)
17 {
18  int displayIndex = 0;
19  SDL_DisplayMode currentMode;
20  if (SDL_GetCurrentDisplayMode(displayIndex, &currentMode) != 0) {
21  // Error. Can this happen? Anything we can do?
22  assert(false);
23  }
24  int bpp;
25  Uint32 rmask, gmask, bmask, amask;
26  SDL_PixelFormatEnumToMasks(
27  currentMode.format, &bpp, &rmask, &gmask, &bmask, &amask);
28  if (bpp == 32) {
29  if (amask == 0) {
30  amask = ~(rmask | gmask | bmask);
31  }
32  } else { // TODO should we also check {R,G,B}_loss == 0?
33  // Use ARGB8888 as a fallback
34  amask = 0xff000000;
35  rmask = 0x00ff0000;
36  gmask = 0x0000ff00;
37  bmask = 0x000000ff;
38  }
39 
40  return SDLSurfacePtr(abs(size_[0]), abs(size_[1]), 32,
41  rmask, gmask, bmask, amask);
42 }
43 
44 // Helper functions to draw a gradient
45 // Extract R,G,B,A components to 8.16 bit fixed point.
46 // Note the order R,G,B,A is arbitrary, the actual pixel value may have the
47 // components in a different order.
48 struct UnpackedRGBA {
49  unsigned r, g, b, a;
50 };
51 static constexpr UnpackedRGBA unpackRGBA(unsigned rgba)
52 {
53  unsigned r = (((rgba >> 24) & 0xFF) << 16) + 0x8000;
54  unsigned g = (((rgba >> 16) & 0xFF) << 16) + 0x8000;
55  unsigned b = (((rgba >> 8) & 0xFF) << 16) + 0x8000;
56  unsigned a = (((rgba >> 0) & 0xFF) << 16) + 0x8000;
57  return {r, g, b, a};
58 }
59 // Setup outer loop (vertical) interpolation parameters.
60 // For each component there is a pair of (initial,delta) values. These values
61 // are 8.16 bit fixed point, delta is signed.
62 struct Interp1Result {
63  unsigned r0, g0, b0, a0;
64  int dr, dg, db, da;
65 };
66 static constexpr Interp1Result setupInterp1(unsigned rgba0, unsigned rgba1, unsigned length)
67 {
68  auto [r0, g0, b0, a0] = unpackRGBA(rgba0);
69  if (length == 1) {
70  return {r0, g0, b0, a0,
71  0, 0, 0, 0};
72  } else {
73  auto [r1, g1, b1, a1] = unpackRGBA(rgba1);
74  int dr = int(r1 - r0) / int(length - 1);
75  int dg = int(g1 - g0) / int(length - 1);
76  int db = int(b1 - b0) / int(length - 1);
77  int da = int(a1 - a0) / int(length - 1);
78  return {r0, g0, b0, a0,
79  dr, dg, db, da};
80  }
81 }
82 // Setup inner loop (horizontal) interpolation parameters.
83 // - Like above we also output a pair of (initial,delta) values for each
84 // component. But we pack two components in one 32-bit value. This leaves only
85 // 16 bits per component, so now the values are 8.8 bit fixed point.
86 // - To avoid carry/borrow from the lower to the upper pack, we make the lower
87 // component always a positive number and output a boolean to indicate whether
88 // we should add or subtract the delta from the initial value.
89 // - The 8.8 fixed point calculations in the inner loop are less accurate than
90 // the 8.16 calculations in the outer loop. This could result in not 100%
91 // accurate gradients. Though only on very wide images and the error is
92 // so small that it will hardly be visible (if at all).
93 // - Packing 2 components in one value is not beneficial in the outer loop
94 // because in this routine we need the individual components of the values
95 // that are calculated by setupInterp1(). (It would also make the code even
96 // more complex).
97 struct Interp2Result {
98  unsigned rb, ga;
99  unsigned drb, dga;
100  bool subRB, subGA;
101 };
102 static constexpr Interp2Result setupInterp2(
103  unsigned r0, unsigned g0, unsigned b0, unsigned a0,
104  unsigned r1, unsigned g1, unsigned b1, unsigned a1,
105  unsigned length)
106 {
107  // Pack the initial values for the components R,B and G,A into
108  // a vector-type: two 8.16 scalars -> one [8.8 ; 8.8] vector
109  unsigned rb = ((r0 << 8) & 0xffff0000) |
110  ((b0 >> 8) & 0x0000ffff);
111  unsigned ga = ((g0 << 8) & 0xffff0000) |
112  ((a0 >> 8) & 0x0000ffff);
113  if (length == 1) {
114  return {rb, ga, 0, 0, false, false};
115  } else {
116  // calculate delta values
117  bool subRB = false;
118  bool subGA = false;
119  int dr = int(r1 - r0) / int(length - 1);
120  int dg = int(g1 - g0) / int(length - 1);
121  int db = int(b1 - b0) / int(length - 1);
122  int da = int(a1 - a0) / int(length - 1);
123  if (db < 0) { // make sure db is positive
124  dr = -dr;
125  db = -db;
126  subRB = true;
127  }
128  if (da < 0) { // make sure da is positive
129  dg = -dg;
130  da = -da;
131  subGA = true;
132  }
133  // also pack two 8.16 delta values in one [8.8 ; 8.8] vector
134  unsigned drb = ((unsigned(dr) << 8) & 0xffff0000) |
135  ((unsigned(db) >> 8) & 0x0000ffff);
136  unsigned dga = ((unsigned(dg) << 8) & 0xffff0000) |
137  ((unsigned(da) >> 8) & 0x0000ffff);
138  return {rb, ga, drb, dga, subRB, subGA};
139  }
140 }
141 // Pack two [8.8 ; 8.8] vectors into one pixel.
142 static constexpr unsigned packRGBA(unsigned rb, unsigned ga)
143 {
144  return (rb & 0xff00ff00) | ((ga & 0xff00ff00) >> 8);
145 }
146 
147 // Draw a gradient on the given surface. This is a bilinear interpolation
148 // between 4 RGBA colors. One color for each corner, in this order:
149 // 0 -- 1
150 // | |
151 // 2 -- 3
152 static constexpr void gradient(const unsigned* rgba, SDL_Surface& surface, unsigned borderSize)
153 {
154  int width = surface.w - 2 * borderSize;
155  int height = surface.h - 2 * borderSize;
156  if ((width <= 0) || (height <= 0)) return;
157 
158  auto [r0, g0, b0, a0, dr02, dg02, db02, da02] = setupInterp1(rgba[0], rgba[2], height);
159  auto [r1, g1, b1, a1, dr13, dg13, db13, da13] = setupInterp1(rgba[1], rgba[3], height);
160 
161  auto* buffer = static_cast<unsigned*>(surface.pixels);
162  buffer += borderSize;
163  buffer += borderSize * (surface.pitch / sizeof(unsigned));
164  repeat(height, [&, r0=r0, g0=g0, b0=b0, a0=a0, dr02=dr02, dg02=dg02, db02=db02, da02=da02,
165  r1=r1, g1=g1, b1=b1, a1=a1, dr13=dr13, dg13=dg13, db13=db13, da13=da13] () mutable {
166  auto [rb, ga, drb, dga, subRB, subGA] = setupInterp2(r0, g0, b0, a0, r1, g1, b1, a1, width);
167 
168  // Depending on the subRB/subGA booleans, we need to add or
169  // subtract the delta to/from the initial value. There are
170  // 2 booleans so 4 combinations:
171  if (!subRB) {
172  if (!subGA) {
173  for (auto x : xrange(width)) {
174  buffer[x] = packRGBA(rb, ga);
175  rb += drb; ga += dga;
176  }
177  } else {
178  for (auto x : xrange(width)) {
179  buffer[x] = packRGBA(rb, ga);
180  rb += drb; ga -= dga;
181  }
182  }
183  } else {
184  if (!subGA) {
185  for (auto x : xrange(width)) {
186  buffer[x] = packRGBA(rb, ga);
187  rb -= drb; ga += dga;
188  }
189  } else {
190  for (auto x : xrange(width)) {
191  buffer[x] = packRGBA(rb, ga);
192  rb -= drb; ga -= dga;
193  }
194  }
195  }
196 
197  r0 += dr02; g0 += dg02; b0 += db02; a0 += da02;
198  r1 += dr13; g1 += dg13; b1 += db13; a1 += da13;
199  buffer += (surface.pitch / sizeof(unsigned));
200  });
201 }
202 
203 // class SDLImage
204 
205 SDLImage::SDLImage(OutputSurface& output, const std::string& filename)
206  : texture(loadImage(output, filename))
207  , flipX(false), flipY(false)
208 {
209 }
210 
211 // TODO get rid of this constructor
212 // instead allow to draw the same SDLImage to different sizes
213 SDLImage::SDLImage(OutputSurface& output, const std::string& filename, float scaleFactor)
214  : texture(loadImage(output, filename))
215  , flipX(scaleFactor < 0.0f), flipY(scaleFactor < 0.0f)
216 {
217  size = trunc(vec2(size) * std::abs(scaleFactor)); // scale image size
218 }
219 
220 // TODO get rid of this constructor, see above
221 SDLImage::SDLImage(OutputSurface& output, const std::string& filename, ivec2 size_)
222  : texture(loadImage(output, filename))
223  , flipX(size_[0] < 0), flipY(size_[1] < 0)
224 {
225  size = size_; // replace image size
226 }
227 
228 SDLImage::SDLImage(OutputSurface& output, ivec2 size_, unsigned rgba)
229  : flipX(size_[0] < 0), flipY(size_[1] < 0)
230 {
231  initSolid(output, size_, rgba, 0, 0); // no border
232 }
233 
234 
236  unsigned borderSize, unsigned borderRGBA)
237  : flipX(size_[0] < 0), flipY(size_[1] < 0)
238 {
239  if ((rgba[0] == rgba[1]) &&
240  (rgba[0] == rgba[2]) &&
241  (rgba[0] == rgba[3])) {
242  initSolid (output, size_, rgba[0], borderSize, borderRGBA);
243  } else {
244  initGradient(output, size_, rgba, borderSize, borderRGBA);
245  }
246 }
247 
249  : texture(toTexture(output, *image))
250  , flipX(false), flipY(false)
251 {
252 }
253 
254 SDLTexturePtr SDLImage::toTexture(OutputSurface& output, SDL_Surface& surface)
255 {
256  SDLTexturePtr result(SDL_CreateTextureFromSurface(
257  checked_cast<SDLOutputSurface&>(output).getSDLRenderer(), &surface));
258  SDL_SetTextureBlendMode(result.get(), SDL_BLENDMODE_BLEND);
259  SDL_QueryTexture(result.get(), nullptr, nullptr, &size[0], &size[1]);
260  return result;
261 }
262 
263 SDLTexturePtr SDLImage::loadImage(OutputSurface& output, const std::string& filename)
264 {
265  bool want32bpp = true;
266  return toTexture(output, *PNG::load(filename, want32bpp));
267 }
268 
269 
270 static unsigned convertColor(const SDL_PixelFormat& format, unsigned rgba)
271 {
272  return SDL_MapRGBA(
273  &format,
274  (rgba >> 24) & 0xff,
275  (rgba >> 16) & 0xff,
276  (rgba >> 8) & 0xff,
277  (rgba >> 0) & 0xff);
278 }
279 
280 static void drawBorder(SDL_Surface& image, int size, unsigned rgba)
281 {
282  if (size <= 0) return;
283 
284  unsigned color = convertColor(*image.format, rgba);
285  bool onlyBorder = ((2 * size) >= image.w) ||
286  ((2 * size) >= image.h);
287  if (onlyBorder) {
288  SDL_FillRect(&image, nullptr, color);
289  } else {
290  // +--------------------+
291  // | 1 |
292  // +---+------------+---+
293  // | | | |
294  // | 3 | | 4 |
295  // | | | |
296  // +---+------------+---+
297  // | 2 |
298  // +--------------------+
299  SDL_Rect rect;
300  rect.x = 0;
301  rect.y = 0;
302  rect.w = image.w;
303  rect.h = size;
304  SDL_FillRect(&image, &rect, color); // 1
305 
306  rect.y = image.h - size;
307  SDL_FillRect(&image, &rect, color); // 2
308 
309  rect.y = size;
310  rect.w = size;
311  rect.h = image.h - 2 * size;
312  SDL_FillRect(&image, &rect, color); // 3
313 
314  rect.x = image.w - size;
315  SDL_FillRect(&image, &rect, color); // 4
316  }
317 }
318 
319 void SDLImage::initSolid(OutputSurface& output, ivec2 size_, unsigned rgba,
320  unsigned borderSize, unsigned borderRGBA)
321 {
322  checkSize(size_);
323  if ((size_[0] == 0) || (size_[1] == 0)) {
324  // SDL_FillRect crashes on zero-width surfaces, so check for it
325  return;
326  }
327 
328  SDLSurfacePtr tmp32 = getTempSurface(size_);
329 
330  // draw interior
331  SDL_FillRect(tmp32.get(), nullptr, convertColor(*tmp32->format, rgba));
332 
333  drawBorder(*tmp32, borderSize, borderRGBA);
334 
335  texture = toTexture(output, *tmp32);
336 }
337 
338 void SDLImage::initGradient(OutputSurface& output, ivec2 size_, span<const unsigned, 4> rgba_,
339  unsigned borderSize, unsigned borderRGBA)
340 {
341  checkSize(size_);
342  if ((size_[0] == 0) || (size_[1] == 0)) {
343  return;
344  }
345 
346  unsigned rgba[4];
347  ranges::copy(rgba_, rgba);
348 
349  if (flipX) {
350  std::swap(rgba[0], rgba[1]);
351  std::swap(rgba[2], rgba[3]);
352  }
353  if (flipY) {
354  std::swap(rgba[0], rgba[2]);
355  std::swap(rgba[1], rgba[3]);
356  }
357 
358  SDLSurfacePtr tmp32 = getTempSurface(size_);
359  for (auto& c : rgba) {
360  c = convertColor(*tmp32->format, c);
361  }
362  gradient(rgba, *tmp32, borderSize);
363  drawBorder(*tmp32, borderSize, borderRGBA);
364 
365  texture = toTexture(output, *tmp32);
366 }
367 
368 void SDLImage::draw(OutputSurface& output, gl::ivec2 pos, uint8_t r, uint8_t g, uint8_t b, uint8_t alpha)
369 {
370  assert(r == 255); (void)r; // SDL2 supports this now, but do we need it?
371  assert(g == 255); (void)g;
372  assert(b == 255); (void)b;
373 
374  if (!texture) return;
375 
376  auto [x, y] = pos;
377  auto [w, h] = size;
378  if (flipX) x -= w;
379  if (flipY) y -= h;
380 
381  auto* renderer = checked_cast<SDLOutputSurface&>(output).getSDLRenderer();
382  SDL_SetTextureAlphaMod(texture.get(), alpha);
383  SDL_Rect dst = {x, y, w, h};
384  SDL_RenderCopy(renderer, texture.get(), nullptr, &dst);
385 }
386 
387 } // namespace openmsx
std::string image
Definition: HDImageCLI.cc:13
void swap(openmsx::MemBuffer< T > &l, openmsx::MemBuffer< T > &r) noexcept
Definition: MemBuffer.hh:202
std::unique_ptr< SDL_Texture, SDLDestroyTexture > SDLTexturePtr
int g
Wrapper around a SDL_Surface.
SDL_Surface * get()
gl::ivec2 size
Definition: BaseImage.hh:31
static void checkSize(gl::ivec2 size)
Performs a sanity check on image size.
Definition: BaseImage.cc:8
A frame buffer where pixels can be written to.
SDLImage(OutputSurface &output, const std::string &filename)
Definition: SDLImage.cc:205
void draw(OutputSurface &output, gl::ivec2 pos, uint8_t r, uint8_t g, uint8_t b, uint8_t alpha) override
Definition: SDLImage.cc:368
Definition: span.hh:126
constexpr T abs(T t)
Definition: cstd.hh:125
Definition: gl_mat.hh:23
vecN< 2, float > vec2
Definition: gl_vec.hh:139
T length(const vecN< N, T > &x)
Definition: gl_vec.hh:343
constexpr vecN< N, int > trunc(const vecN< N, T > &x)
Definition: gl_vec.hh:378
SDLSurfacePtr load(const std::string &filename, bool want32bpp)
Load the given PNG file in a SDL_Surface.
Definition: PNG.cc:95
This file implemented 3 utility functions:
Definition: Autofire.cc:9
constexpr const char *const filename
constexpr KeyMatrixPosition x
Keyboard bindings.
Definition: Keyboard.cc:118
auto copy(InputRange &&range, OutputIter out)
Definition: ranges.hh:179
size_t size(std::string_view utf8)
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition: xrange.hh:170
constexpr auto xrange(T e)
Definition: xrange.hh:155