1/**
2 * Copyright (c) 2006-2023 LOVE Development Team
3 *
4 * This software is provided 'as-is', without any express or implied
5 * warranty. In no event will the authors be held liable for any damages
6 * arising from the use of this software.
7 *
8 * Permission is granted to anyone to use this software for any purpose,
9 * including commercial applications, and to alter it and redistribute it
10 * freely, subject to the following restrictions:
11 *
12 * 1. The origin of this software must not be misrepresented; you must not
13 * claim that you wrote the original software. If you use this software
14 * in a product, an acknowledgment in the product documentation would be
15 * appreciated but is not required.
16 * 2. Altered source versions must be plainly marked as such, and must not be
17 * misrepresented as being the original software.
18 * 3. This notice may not be removed or altered from any source distribution.
19 **/
20
21#include "Image.h"
22#include "Graphics.h"
23
24// C++
25#include <algorithm>
26
27namespace love
28{
29namespace graphics
30{
31
32love::Type Image::type("Image", &Texture::type);
33
34int Image::imageCount = 0;
35
36Image::Image(const Slices &data, const Settings &settings, bool validatedata)
37 : Texture(data.getTextureType())
38 , settings(settings)
39 , data(data)
40 , mipmapsType(settings.mipmaps ? MIPMAPS_GENERATED : MIPMAPS_NONE)
41 , sRGB(isGammaCorrect() && !settings.linear)
42 , usingDefaultTexture(false)
43{
44 if (validatedata && data.validate() == MIPMAPS_DATA)
45 mipmapsType = MIPMAPS_DATA;
46}
47
48Image::Image(TextureType textype, PixelFormat format, int width, int height, int slices, const Settings &settings)
49 : Image(Slices(textype), settings, false)
50{
51 if (isPixelFormatCompressed(format))
52 throw love::Exception("This constructor is only supported for non-compressed pixel formats.");
53
54 if (textype == TEXTURE_2D_ARRAY)
55 layers = slices;
56 else if (textype == TEXTURE_VOLUME)
57 depth = slices;
58
59 init(format, width, height, settings);
60}
61
62Image::Image(const Slices &slices, const Settings &settings)
63 : Image(slices, settings, true)
64{
65 if (texType == TEXTURE_2D_ARRAY)
66 this->layers = data.getSliceCount();
67 else if (texType == TEXTURE_VOLUME)
68 this->depth = data.getSliceCount();
69
70 love::image::ImageDataBase *slice = data.get(0, 0);
71 init(slice->getFormat(), slice->getWidth(), slice->getHeight(), settings);
72}
73
74Image::~Image()
75{
76 --imageCount;
77}
78
79void Image::init(PixelFormat fmt, int w, int h, const Settings &settings)
80{
81 Graphics *gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);
82 if (gfx != nullptr && !gfx->isImageFormatSupported(fmt, sRGB))
83 {
84 const char *str;
85 if (love::getConstant(fmt, str))
86 {
87 throw love::Exception("Cannot create image: "
88 "%s%s images are not supported on this system.", sRGB ? "sRGB " : "", str);
89 }
90 else
91 throw love::Exception("cannot create image: format is not supported on this system.");
92 }
93
94 pixelWidth = w;
95 pixelHeight = h;
96
97 width = (int) (pixelWidth / settings.dpiScale + 0.5);
98 height = (int) (pixelHeight / settings.dpiScale + 0.5);
99
100 format = fmt;
101
102 if (isCompressed() && mipmapsType == MIPMAPS_GENERATED)
103 mipmapsType = MIPMAPS_NONE;
104
105 mipmapCount = mipmapsType == MIPMAPS_NONE ? 1 : getTotalMipmapCount(w, h, depth);
106
107 if (mipmapCount > 1)
108 filter.mipmap = defaultMipmapFilter;
109
110 initQuad();
111
112 ++imageCount;
113}
114
115void Image::uploadImageData(love::image::ImageDataBase *d, int level, int slice, int x, int y)
116{
117 love::image::ImageData *id = dynamic_cast<love::image::ImageData *>(d);
118
119 love::thread::EmptyLock lock;
120 if (id != nullptr)
121 lock.setLock(id->getMutex());
122
123 Rect rect = {x, y, d->getWidth(), d->getHeight()};
124 uploadByteData(d->getFormat(), d->getData(), d->getSize(), level, slice, rect);
125}
126
127void Image::replacePixels(love::image::ImageDataBase *d, int slice, int mipmap, int x, int y, bool reloadmipmaps)
128{
129 // No effect if the texture hasn't been created yet.
130 if (getHandle() == 0 || usingDefaultTexture)
131 return;
132
133 if (d->getFormat() != getPixelFormat())
134 throw love::Exception("Pixel formats must match.");
135
136 if (mipmap < 0 || (mipmapsType != MIPMAPS_DATA && mipmap > 0) || mipmap >= getMipmapCount())
137 throw love::Exception("Invalid image mipmap index %d.", mipmap + 1);
138
139 if (slice < 0 || (texType == TEXTURE_CUBE && slice >= 6)
140 || (texType == TEXTURE_VOLUME && slice >= getDepth(mipmap))
141 || (texType == TEXTURE_2D_ARRAY && slice >= getLayerCount()))
142 {
143 throw love::Exception("Invalid image slice index %d.", slice + 1);
144 }
145
146 Rect rect = {x, y, d->getWidth(), d->getHeight()};
147
148 int mipw = getPixelWidth(mipmap);
149 int miph = getPixelHeight(mipmap);
150
151 if (rect.x < 0 || rect.y < 0 || rect.w <= 0 || rect.h <= 0
152 || (rect.x + rect.w) > mipw || (rect.y + rect.h) > miph)
153 {
154 throw love::Exception("Invalid rectangle dimensions (x=%d, y=%d, w=%d, h=%d) for %dx%d Image.", rect.x, rect.y, rect.w, rect.h, mipw, miph);
155 }
156
157 love::image::ImageDataBase *oldd = data.get(slice, mipmap);
158
159 if (oldd == nullptr)
160 throw love::Exception("Image does not store ImageData!");
161
162 Rect currect = {0, 0, oldd->getWidth(), oldd->getHeight()};
163
164 // We can only replace the internal Data (used when reloading due to setMode)
165 // if the dimensions match. We also don't currently support partial updates
166 // of compressed textures.
167 if (rect == currect)
168 data.set(slice, mipmap, d);
169 else if (isPixelFormatCompressed(d->getFormat()))
170 throw love::Exception("Compressed textures only support replacing the entire Image.");
171
172 Graphics::flushStreamDrawsGlobal();
173
174 uploadImageData(d, mipmap, slice, x, y);
175
176 if (reloadmipmaps && mipmap == 0 && getMipmapCount() > 1)
177 generateMipmaps();
178}
179
180void Image::replacePixels(const void *data, size_t size, int slice, int mipmap, const Rect &rect, bool reloadmipmaps)
181{
182 Graphics::flushStreamDrawsGlobal();
183
184 uploadByteData(format, data, size, mipmap, slice, rect);
185
186 if (reloadmipmaps && mipmap == 0 && getMipmapCount() > 1)
187 generateMipmaps();
188}
189
190bool Image::isCompressed() const
191{
192 return isPixelFormatCompressed(format);
193}
194
195bool Image::isFormatLinear() const
196{
197 return isGammaCorrect() && !sRGB;
198}
199
200Image::MipmapsType Image::getMipmapsType() const
201{
202 return mipmapsType;
203}
204
205Image::Slices::Slices(TextureType textype)
206 : textureType(textype)
207{
208}
209
210void Image::Slices::clear()
211{
212 data.clear();
213}
214
215void Image::Slices::set(int slice, int mipmap, love::image::ImageDataBase *d)
216{
217 if (textureType == TEXTURE_VOLUME)
218 {
219 if (mipmap >= (int) data.size())
220 data.resize(mipmap + 1);
221
222 if (slice >= (int) data[mipmap].size())
223 data[mipmap].resize(slice + 1);
224
225 data[mipmap][slice].set(d);
226 }
227 else
228 {
229 if (slice >= (int) data.size())
230 data.resize(slice + 1);
231
232 if (mipmap >= (int) data[slice].size())
233 data[slice].resize(mipmap + 1);
234
235 data[slice][mipmap].set(d);
236 }
237}
238
239love::image::ImageDataBase *Image::Slices::get(int slice, int mipmap) const
240{
241 if (slice < 0 || slice >= getSliceCount(mipmap))
242 return nullptr;
243
244 if (mipmap < 0 || mipmap >= getMipmapCount(slice))
245 return nullptr;
246
247 if (textureType == TEXTURE_VOLUME)
248 return data[mipmap][slice].get();
249 else
250 return data[slice][mipmap].get();
251}
252
253void Image::Slices::add(love::image::CompressedImageData *cdata, int startslice, int startmip, bool addallslices, bool addallmips)
254{
255 int slicecount = addallslices ? cdata->getSliceCount() : 1;
256 int mipcount = addallmips ? cdata->getMipmapCount() : 1;
257
258 for (int mip = 0; mip < mipcount; mip++)
259 {
260 for (int slice = 0; slice < slicecount; slice++)
261 set(startslice + slice, startmip + mip, cdata->getSlice(slice, mip));
262 }
263}
264
265int Image::Slices::getSliceCount(int mip) const
266{
267 if (textureType == TEXTURE_VOLUME)
268 {
269 if (mip < 0 || mip >= (int) data.size())
270 return 0;
271
272 return (int) data[mip].size();
273 }
274 else
275 return (int) data.size();
276}
277
278int Image::Slices::getMipmapCount(int slice) const
279{
280 if (textureType == TEXTURE_VOLUME)
281 return (int) data.size();
282 else
283 {
284 if (slice < 0 || slice >= (int) data.size())
285 return 0;
286
287 return data[slice].size();
288 }
289}
290
291Image::MipmapsType Image::Slices::validate() const
292{
293 int slicecount = getSliceCount();
294 int mipcount = getMipmapCount(0);
295
296 if (slicecount == 0 || mipcount == 0)
297 throw love::Exception("At least one ImageData or CompressedImageData is required!");
298
299 if (textureType == TEXTURE_CUBE && slicecount != 6)
300 throw love::Exception("Cube textures must have exactly 6 sides.");
301
302 image::ImageDataBase *firstdata = get(0, 0);
303
304 int w = firstdata->getWidth();
305 int h = firstdata->getHeight();
306 int depth = textureType == TEXTURE_VOLUME ? slicecount : 1;
307 PixelFormat format = firstdata->getFormat();
308
309 int expectedmips = Texture::getTotalMipmapCount(w, h, depth);
310
311 if (mipcount != expectedmips && mipcount != 1)
312 throw love::Exception("Image does not have all required mipmap levels (expected %d, got %d)", expectedmips, mipcount);
313
314 if (textureType == TEXTURE_CUBE && w != h)
315 throw love::Exception("Cube images must have equal widths and heights for each cube face.");
316
317 int mipw = w;
318 int miph = h;
319 int mipslices = slicecount;
320
321 for (int mip = 0; mip < mipcount; mip++)
322 {
323 if (textureType == TEXTURE_VOLUME)
324 {
325 slicecount = getSliceCount(mip);
326
327 if (slicecount != mipslices)
328 throw love::Exception("Invalid number of image data layers in mipmap level %d (expected %d, got %d)", mip+1, mipslices, slicecount);
329 }
330
331 for (int slice = 0; slice < slicecount; slice++)
332 {
333 auto slicedata = get(slice, mip);
334
335 if (slicedata == nullptr)
336 throw love::Exception("Missing image data (slice %d, mipmap level %d)", slice+1, mip+1);
337
338 int realw = slicedata->getWidth();
339 int realh = slicedata->getHeight();
340
341 if (getMipmapCount(slice) != mipcount)
342 throw love::Exception("All Image layers must have the same mipmap count.");
343
344 if (mipw != realw)
345 throw love::Exception("Width of image data (slice %d, mipmap level %d) is incorrect (expected %d, got %d)", slice+1, mip+1, mipw, realw);
346
347 if (miph != realh)
348 throw love::Exception("Height of image data (slice %d, mipmap level %d) is incorrect (expected %d, got %d)", slice+1, mip+1, miph, realh);
349
350 if (format != slicedata->getFormat())
351 throw love::Exception("All Image slices and mipmaps must have the same pixel format.");
352 }
353
354 mipw = std::max(mipw / 2, 1);
355 miph = std::max(miph / 2, 1);
356
357 if (textureType == TEXTURE_VOLUME)
358 mipslices = std::max(mipslices / 2, 1);
359 }
360
361 if (mipcount > 1)
362 return MIPMAPS_DATA;
363 else
364 return MIPMAPS_NONE;
365}
366
367bool Image::getConstant(const char *in, SettingType &out)
368{
369 return settingTypes.find(in, out);
370}
371
372bool Image::getConstant(SettingType in, const char *&out)
373{
374 return settingTypes.find(in, out);
375}
376
377const char *Image::getConstant(SettingType in)
378{
379 const char *name = nullptr;
380 getConstant(in, name);
381 return name;
382}
383
384std::vector<std::string> Image::getConstants(SettingType)
385{
386 return settingTypes.getNames();
387}
388
389StringMap<Image::SettingType, Image::SETTING_MAX_ENUM>::Entry Image::settingTypeEntries[] =
390{
391 { "mipmaps", SETTING_MIPMAPS },
392 { "linear", SETTING_LINEAR },
393 { "dpiscale", SETTING_DPI_SCALE },
394};
395
396StringMap<Image::SettingType, Image::SETTING_MAX_ENUM> Image::settingTypes(Image::settingTypeEntries, sizeof(Image::settingTypeEntries));
397
398} // graphics
399} // love
400