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
12using namespace gl;
13
14namespace openmsx {
15
16static 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 {unsigned(abs(size_[0])), unsigned(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.
49 unsigned r, g, b, a;
50};
51static 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.
63 unsigned r0, g0, b0, a0;
64 int dr, dg, db, da;
65};
66static 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).
98 unsigned rb, ga;
99 unsigned drb, dga;
101};
102static 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.
142static 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
152static 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
205SDLImage::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
213SDLImage::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
221SDLImage::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
228SDLImage::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
235SDLImage::SDLImage(OutputSurface& output, ivec2 size_, std::span<const unsigned, 4> rgba,
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
254SDLTexturePtr 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
263SDLTexturePtr SDLImage::loadImage(OutputSurface& output, const std::string& filename)
264{
265 bool want32bpp = true;
266 return toTexture(output, *PNG::load(filename, want32bpp));
267}
268
269
270static 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
280static 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
319void 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
338void SDLImage::initGradient(OutputSurface& output, ivec2 size_, std::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
368void 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
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
constexpr T abs(T t)
Definition: cstd.hh:16
Definition: gl_mat.hh:23
constexpr vecN< N, int > trunc(const vecN< N, T > &x)
Definition: gl_vec.hh:374
vecN< 2, float > vec2
Definition: gl_vec.hh:148
T length(const vecN< N, T > &x)
Definition: gl_vec.hh:339
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:127
auto copy(InputRange &&range, OutputIter out)
Definition: ranges.hh:208
void swap(openmsx::MemBuffer< T > &l, openmsx::MemBuffer< T > &r) noexcept
Definition: MemBuffer.hh:202
size_t size(std::string_view utf8)
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition: xrange.hh:148
constexpr auto xrange(T e)
Definition: xrange.hh:133