1 /*
2 	This file is part of Warzone 2100.
3 	Copyright (C) 1999-2004  Eidos Interactive
4 	Copyright (C) 2005-2020  Warzone 2100 Project
5 
6 	Warzone 2100 is free software; you can redistribute it and/or modify
7 	it under the terms of the GNU General Public License as published by
8 	the Free Software Foundation; either version 2 of the License, or
9 	(at your option) any later version.
10 
11 	Warzone 2100 is distributed in the hope that it will be useful,
12 	but WITHOUT ANY WARRANTY; without even the implied warranty of
13 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 	GNU General Public License for more details.
15 
16 	You should have received a copy of the GNU General Public License
17 	along with Warzone 2100; if not, write to the Free Software
18 	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 
21 #include "lib/framework/frame.h"
22 #include "lib/framework/frameresource.h"
23 
24 #include "lib/ivis_opengl/ivisdef.h"
25 #include "lib/ivis_opengl/piestate.h"
26 #include "lib/ivis_opengl/tex.h"
27 #include "lib/ivis_opengl/piepalette.h"
28 #include "lib/ivis_opengl/png_util.h"
29 
30 #include "screen.h"
31 
32 #include <algorithm>
33 #include <unordered_map>
34 
35 #if defined(__clang__)
36 #  pragma clang diagnostic push
37 #  pragma clang diagnostic ignored "-Wcast-align"
38 #endif
39 #if defined(__GNUC__)
40 #  pragma GCC diagnostic push
41 #  pragma GCC diagnostic ignored "-Wcast-qual"
42 #endif
43 
44 #include "3rdparty/stb_image_resize.h"
45 
46 #if defined(__GNUC__)
47 #  pragma GCC diagnostic pop
48 #endif
49 #if defined(__clang__)
50 #  pragma clang diagnostic pop
51 #endif
52 
53 //*************************************************************************
54 
55 struct iTexPage
56 {
57 	std::string name;
58 	gfx_api::texture* id = nullptr;
59 
60 	iTexPage() = default;
61 
iTexPageiTexPage62 	iTexPage(iTexPage&& input)
63 	{
64 		std::swap(name, input.name);
65 		id = input.id;
66 		input.id = nullptr;
67 	}
68 
~iTexPageiTexPage69 	~iTexPage()
70 	{
71 		if (id)
72 			delete id;
73 	}
74 
75 	iTexPage (const iTexPage &) = delete;
76 	iTexPage & operator = (const iTexPage &) = delete;
77 };
78 
79 std::vector<iTexPage> _TEX_PAGE;
80 std::unordered_map<std::string, size_t> _NAME_TO_TEX_PAGE_MAP;
81 
82 //*************************************************************************
83 
pie_Texture(size_t page)84 gfx_api::texture& pie_Texture(size_t page)
85 {
86 	return *(_TEX_PAGE[page].id);
87 }
88 
pie_NumberOfPages()89 size_t pie_NumberOfPages()
90 {
91 	return _TEX_PAGE.size();
92 }
93 
94 // Add a new texture page to the list
pie_ReserveTexture(const char * name,const size_t & width,const size_t & height)95 size_t pie_ReserveTexture(const char *name, const size_t& width, const size_t& height)
96 {
97 	iTexPage tex;
98 	tex.name = name;
99 	_TEX_PAGE.push_back(std::move(tex));
100 	size_t page = _TEX_PAGE.size() - 1;
101 	_NAME_TO_TEX_PAGE_MAP[name] = page;
102 	return page;
103 }
104 
pie_AssignTexture(size_t page,gfx_api::texture * texture)105 void pie_AssignTexture(size_t page, gfx_api::texture* texture)
106 {
107 	if (_TEX_PAGE[page].id)
108 		delete _TEX_PAGE[page].id;
109 	_TEX_PAGE[page].id = texture;
110 }
111 
pie_AddTexPage_Impl(iV_Image * s,const char * filename,bool gameTexture,size_t page)112 static size_t pie_AddTexPage_Impl(iV_Image *s, const char *filename, bool gameTexture, size_t page)
113 {
114 	ASSERT(s && filename, "Bad input parameter");
115 
116 	_NAME_TO_TEX_PAGE_MAP[filename] = page;
117 	debug(LOG_TEXTURE, "%s page=%zu", filename, page);
118 
119 	if (gameTexture) // this is a game texture, use texture compression
120 	{
121 		gfx_api::pixel_format format = iV_getPixelFormat(s);
122 		if (_TEX_PAGE[page].id)
123 			delete _TEX_PAGE[page].id;
124 		size_t mip_count = floor(log(std::max(s->width, s->height))) + 1;
125 		_TEX_PAGE[page].id = gfx_api::context::get().create_texture(mip_count, s->width, s->height, format, filename);
126 		pie_Texture(page).upload_and_generate_mipmaps(0u, 0u, s->width, s->height, iV_getPixelFormat(s), s->bmp);
127 	}
128 	else	// this is an interface texture, do not use compression
129 	{
130 		if (_TEX_PAGE[page].id)
131 			delete _TEX_PAGE[page].id;
132 		_TEX_PAGE[page].id = gfx_api::context::get().create_texture(1, s->width, s->height, gfx_api::pixel_format::FORMAT_RGBA8_UNORM_PACK8, filename);
133 		pie_Texture(page).upload(0u, 0u, 0u, s->width, s->height, iV_getPixelFormat(s), s->bmp);
134 	}
135 
136 	// it is uploaded, we do not need it anymore
137 	free(s->bmp);
138 	s->bmp = nullptr;
139 
140 	/* Send back the texpage number so we can store it in the IMD */
141 	return page;
142 }
143 
pie_AddTexPage(iV_Image * s,const char * filename,bool gameTexture)144 size_t pie_AddTexPage(iV_Image *s, const char *filename, bool gameTexture)
145 {
146 	ASSERT(s && filename, "Bad input parameter");
147 	ASSERT(_NAME_TO_TEX_PAGE_MAP.count(filename) == 0, "tex page %s already exists", filename);
148 	iTexPage tex;
149 	size_t page = _TEX_PAGE.size();
150 	tex.name = filename;
151 	_TEX_PAGE.push_back(std::move(tex));
152 
153 	return pie_AddTexPage_Impl(s, filename, gameTexture, page);
154 }
155 
pie_AddTexPage(iV_Image * s,const char * filename,bool gameTexture,size_t page)156 size_t pie_AddTexPage(iV_Image *s, const char *filename, bool gameTexture, size_t page)
157 {
158 	ASSERT(s && filename, "Bad input parameter");
159 
160 	// replace
161 	_NAME_TO_TEX_PAGE_MAP.erase(_TEX_PAGE[page].name);
162 	_TEX_PAGE[page].name = filename;
163 
164 	return pie_AddTexPage_Impl(s, filename, gameTexture, page);
165 }
166 
167 /*!
168  * Turns filename into a pagename if possible
169  * \param[in,out] filename Filename to pagify
170  */
pie_MakeTexPageName(const std::string & filename)171 std::string pie_MakeTexPageName(const std::string& filename)
172 {
173 	size_t c = filename.find(iV_TEXNAME_TCSUFFIX);
174 	if (c != std::string::npos)
175 	{
176 		return filename.substr(0, c + 7);
177 	}
178 	c = filename.find('-', 5);
179 	if (c != std::string::npos)
180 	{
181 		return filename.substr(0, c);
182 	}
183 	return filename;
184 }
185 
186 /*!
187  * Turns page filename into a pagename + tc mask if possible
188  * \param[in,out] filename Filename to pagify
189  */
pie_MakeTexPageTCMaskName(const std::string & filename)190 std::string pie_MakeTexPageTCMaskName(const std::string& filename)
191 {
192 	std::string result = filename;
193 	if (filename.rfind("page-", 0) == 0)
194 	{
195 		// filename starts with "page-"
196 		size_t i;
197 		for (i = 5; i < filename.size() - 1 && isdigit(filename[i]); i++) {}
198 		result = filename.substr(0, i);
199 		result += iV_TEXNAME_TCSUFFIX;
200 	}
201 	return result;
202 }
203 
scaleImageMaxSize(iV_Image * s,int maxWidth,int maxHeight)204 bool scaleImageMaxSize(iV_Image *s, int maxWidth, int maxHeight)
205 {
206 	if ((maxWidth <= 0 || s->width <= maxWidth) && (maxHeight <= 0 || s->height <= maxHeight))
207 	{
208 		return false;
209 	}
210 
211 	double scalingRatio;
212 	double widthRatio = (double)maxWidth / (double)s->width;
213 	double heightRatio = (double)maxHeight / (double)s->height;
214 	if (maxWidth > 0 && maxHeight > 0)
215 	{
216 		scalingRatio = std::min<double>(widthRatio, heightRatio);
217 	}
218 	else
219 	{
220 		scalingRatio = (maxWidth > 0) ? widthRatio : heightRatio;
221 	}
222 
223 	int output_w = static_cast<int>(s->width * scalingRatio);
224 	int output_h = static_cast<int>(s->height * scalingRatio);
225 
226 	unsigned char *output_pixels = (unsigned char *)malloc(static_cast<size_t>(output_w) * static_cast<size_t>(output_h) * s->depth);
227 	stbir_resize_uint8(s->bmp, s->width, s->height, 0,
228 					   output_pixels, output_w, output_h, 0,
229 					   s->depth);
230 	free(s->bmp);
231 	s->width = output_w;
232 	s->height = output_h;
233 	s->bmp = output_pixels;
234 
235 	return true;
236 }
237 
238 /** Retrieve the texture number for a given texture resource.
239  *
240  *  @note We keep textures in a separate data structure _TEX_PAGE apart from the
241  *        normal resource system.
242  *
243  *  @param filename The filename of the texture page to search for.
244  *  @param compression If we need to load it, should we use texture compression?
245  *  @param maxWidth If we need to load it, should we limit the texture width? (Resizes and preserves the texture image's aspect ratio)
246  *  @param maxHeight If we need to load it, should we limit the texture height? (Resizes and preserves the texture image's aspect ratio)
247  *
248  *  @return a non-negative index number for the texture, negative if no texture
249  *          with the given filename could be found
250  */
iV_GetTexture(const char * filename,bool compression,int maxWidth,int maxHeight)251 optional<size_t> iV_GetTexture(const char *filename, bool compression, int maxWidth /*= -1*/, int maxHeight /*= -1*/)
252 {
253 	ASSERT(filename != nullptr, "filename must not be null");
254 	iV_Image sSprite;
255 
256 	/* Have we already loaded this one then? */
257 	std::string path = pie_MakeTexPageName(filename);
258 	const auto it = _NAME_TO_TEX_PAGE_MAP.find(path);
259 	if (it != _NAME_TO_TEX_PAGE_MAP.end())
260 	{
261 		return it->second;
262 	}
263 
264 	// Try to load it
265 	std::string loadPath = "texpages/";
266 	loadPath += filename;
267 	if (!iV_loadImage_PNG(loadPath.c_str(), &sSprite))
268 	{
269 		debug(LOG_ERROR, "Failed to load %s", loadPath.c_str());
270 		return nullopt;
271 	}
272 	scaleImageMaxSize(&sSprite, maxWidth, maxHeight);
273 	size_t page = pie_AddTexPage(&sSprite, path.c_str(), compression);
274 	resDoResLoadCallback(); // ensure loading screen doesn't freeze when loading large images
275 	return optional<size_t>(page);
276 }
277 
replaceTexture(const WzString & oldfile,const WzString & newfile)278 bool replaceTexture(const WzString &oldfile, const WzString &newfile)
279 {
280 	// Load new one to replace it
281 	iV_Image image;
282 	if (!iV_loadImage_PNG(WzString("texpages/" + newfile).toUtf8().c_str(), &image))
283 	{
284 		debug(LOG_ERROR, "Failed to load image: %s", newfile.toUtf8().c_str());
285 		return false;
286 	}
287 	std::string tmpname = pie_MakeTexPageName(oldfile.toUtf8());
288 	// Have we already loaded this one?
289 	const auto it = _NAME_TO_TEX_PAGE_MAP.find(tmpname);
290 	if (it != _NAME_TO_TEX_PAGE_MAP.end())
291 	{
292 		gfx_api::context::get().debugStringMarker("Replacing texture");
293 		size_t page = it->second;
294 		debug(LOG_TEXTURE, "Replacing texture %s with %s from index %zu (tex id %u)", it->first.c_str(), newfile.toUtf8().c_str(), page, _TEX_PAGE[page].id->id());
295 		tmpname = pie_MakeTexPageName(newfile.toUtf8());
296 		pie_AddTexPage(&image, tmpname.c_str(), true, page);
297 		iV_unloadImage(&image);
298 		return true;
299 	}
300 	iV_unloadImage(&image);
301 	debug(LOG_ERROR, "Nothing to replace!");
302 	return false;
303 }
304 
pie_TexShutDown()305 void pie_TexShutDown()
306 {
307 	// TODO, lazy deletions for faster loading of next level
308 	debug(LOG_TEXTURE, "Cleaning out %u textures", static_cast<unsigned>(_TEX_PAGE.size()));
309 	_TEX_PAGE.clear();
310 	_NAME_TO_TEX_PAGE_MAP.clear();
311 }
312 
pie_TexInit()313 void pie_TexInit()
314 {
315 	debug(LOG_TEXTURE, "pie_TexInit successful");
316 }
317 
iV_unloadImage(iV_Image * image)318 void iV_unloadImage(iV_Image *image)
319 {
320 	if (image)
321 	{
322 		if (image->bmp)
323 		{
324 			free(image->bmp);
325 			image->bmp = nullptr;
326 		}
327 	}
328 	else
329 	{
330 		debug(LOG_ERROR, "Tried to free invalid image!");
331 	}
332 }
333 
iV_getPixelFormat(const iV_Image * image)334 gfx_api::pixel_format iV_getPixelFormat(const iV_Image *image)
335 {
336 	switch (image->depth)
337 	{
338 	case 3:
339 		return gfx_api::pixel_format::FORMAT_RGB8_UNORM_PACK8;
340 	case 4:
341 		return gfx_api::pixel_format::FORMAT_RGBA8_UNORM_PACK8;
342 	default:
343 		debug(LOG_FATAL, "iV_getPixelFormat: Unsupported image depth: %u", image->depth);
344 		return gfx_api::pixel_format::invalid;
345 	}
346 }
347