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