Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "TextureConverter.h"
21 :
22 : #include "lib/allocators/shared_ptr.h"
23 : #include "lib/bits.h"
24 : #include "lib/regex.h"
25 : #include "lib/tex/tex.h"
26 : #include "lib/timer.h"
27 : #include "maths/MD5.h"
28 : #include "ps/CLogger.h"
29 : #include "ps/CStr.h"
30 : #include "ps/Profiler2.h"
31 : #include "ps/Threading.h"
32 : #include "ps/Util.h"
33 : #include "ps/XML/Xeromyces.h"
34 :
35 : #if CONFIG2_NVTT
36 :
37 : #include "nvtt/nvtt.h"
38 :
39 : // We assume NVTT is recent enough to support the alpha flag in the DXT1a format. If users try to use an
40 : // old version of NVTT, the game will crash when trying to decode dds files generated by NVTT.
41 : //
42 : // The support was added upstream in https://github.com/castano/nvidia-texture-tools/commit/782a127071895f538c1ae49925a6e15687e3c966
43 : // so, in theory, 2.0.7 and newer should be enough, but all 2.0.x releases define NVTT_VERSION as 200, so
44 : // we can't distinguish them. NVTT_VERSION is 201 in all development versions of the 2.0.x era, so we also
45 : // have to exclude that value.
46 : #if !defined NVTT_VERSION || NVTT_VERSION == 200 || NVTT_VERSION == 201
47 : #error Please use NVTT 2.1.0 or newer. \
48 : If your system does not provide it, you should use the bundled version by NOT passing --with-system-nvtt to premake.
49 : #endif
50 :
51 : /**
52 : * Output handler to collect NVTT's output into a simplistic buffer.
53 : * WARNING: Used in the worker thread - must be thread-safe.
54 : */
55 12 : struct BufferOutputHandler : public nvtt::OutputHandler
56 : {
57 : std::vector<u8> buffer;
58 :
59 39 : virtual void beginImage(int UNUSED(size), int UNUSED(width), int UNUSED(height), int UNUSED(depth), int UNUSED(face), int UNUSED(miplevel))
60 : {
61 39 : }
62 :
63 45 : virtual bool writeData(const void* data, int size)
64 : {
65 45 : size_t off = buffer.size();
66 45 : buffer.resize(off + size);
67 45 : memcpy(&buffer[off], data, size);
68 45 : return true;
69 : }
70 :
71 39 : virtual void endImage()
72 : {
73 39 : }
74 : };
75 :
76 : /**
77 : * Request for worker thread to process.
78 : */
79 12 : struct CTextureConverter::ConversionRequest
80 : {
81 : VfsPath dest;
82 : CTexturePtr texture;
83 : nvtt::InputOptions inputOptions;
84 : nvtt::CompressionOptions compressionOptions;
85 : nvtt::OutputOptions outputOptions;
86 : };
87 :
88 : /**
89 : * Result from worker thread.
90 : */
91 12 : struct CTextureConverter::ConversionResult
92 : {
93 : VfsPath dest;
94 : CTexturePtr texture;
95 : BufferOutputHandler output;
96 : bool ret; // true if the conversion succeeded
97 : };
98 :
99 : #endif // CONFIG2_NVTT
100 :
101 11 : void CTextureConverter::Settings::Hash(MD5& hash)
102 : {
103 11 : hash.Update((const u8*)&format, sizeof(format));
104 11 : hash.Update((const u8*)&mipmap, sizeof(mipmap));
105 11 : hash.Update((const u8*)&normal, sizeof(normal));
106 11 : hash.Update((const u8*)&alpha, sizeof(alpha));
107 11 : hash.Update((const u8*)&filter, sizeof(filter));
108 11 : hash.Update((const u8*)&kaiserWidth, sizeof(kaiserWidth));
109 11 : hash.Update((const u8*)&kaiserAlpha, sizeof(kaiserAlpha));
110 11 : hash.Update((const u8*)&kaiserStretch, sizeof(kaiserStretch));
111 11 : }
112 :
113 0 : CTextureConverter::SettingsFile* CTextureConverter::LoadSettings(const VfsPath& path) const
114 : {
115 0 : CXeromyces XeroFile;
116 0 : if (XeroFile.Load(m_VFS, path, "texture") != PSRETURN_OK)
117 0 : return NULL;
118 :
119 : // Define all the elements used in the XML file
120 : #define EL(x) int el_##x = XeroFile.GetElementID(#x)
121 : #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
122 0 : EL(textures);
123 0 : EL(file);
124 0 : AT(pattern);
125 0 : AT(format);
126 0 : AT(mipmap);
127 0 : AT(normal);
128 0 : AT(alpha);
129 0 : AT(filter);
130 0 : AT(kaiserwidth);
131 0 : AT(kaiseralpha);
132 0 : AT(kaiserstretch);
133 : #undef AT
134 : #undef EL
135 :
136 0 : XMBElement root = XeroFile.GetRoot();
137 :
138 0 : if (root.GetNodeName() != el_textures)
139 : {
140 0 : LOGERROR("Invalid texture settings file \"%s\" (unrecognised root element)", path.string8());
141 0 : return NULL;
142 : }
143 :
144 0 : std::unique_ptr<SettingsFile> settings = std::make_unique<SettingsFile>();
145 :
146 0 : XERO_ITER_EL(root, child)
147 : {
148 0 : if (child.GetNodeName() == el_file)
149 : {
150 0 : Match p;
151 :
152 0 : XERO_ITER_ATTR(child, attr)
153 : {
154 0 : if (attr.Name == at_pattern)
155 : {
156 0 : p.pattern = attr.Value.FromUTF8();
157 : }
158 0 : else if (attr.Name == at_format)
159 : {
160 0 : CStr v(attr.Value);
161 0 : if (v == "dxt1")
162 0 : p.settings.format = FMT_DXT1;
163 0 : else if (v == "dxt3")
164 0 : p.settings.format = FMT_DXT3;
165 0 : else if (v == "dxt5")
166 0 : p.settings.format = FMT_DXT5;
167 0 : else if (v == "rgba")
168 0 : p.settings.format = FMT_RGBA;
169 0 : else if (v == "alpha")
170 0 : p.settings.format = FMT_ALPHA;
171 : else
172 0 : LOGERROR("Invalid attribute value <file format='%s'>", v.c_str());
173 : }
174 0 : else if (attr.Name == at_mipmap)
175 : {
176 0 : CStr v(attr.Value);
177 0 : if (v == "true")
178 0 : p.settings.mipmap = MIP_TRUE;
179 0 : else if (v == "false")
180 0 : p.settings.mipmap = MIP_FALSE;
181 : else
182 0 : LOGERROR("Invalid attribute value <file mipmap='%s'>", v.c_str());
183 : }
184 0 : else if (attr.Name == at_normal)
185 : {
186 0 : CStr v(attr.Value);
187 0 : if (v == "true")
188 0 : p.settings.normal = NORMAL_TRUE;
189 0 : else if (v == "false")
190 0 : p.settings.normal = NORMAL_FALSE;
191 : else
192 0 : LOGERROR("Invalid attribute value <file normal='%s'>", v.c_str());
193 : }
194 0 : else if (attr.Name == at_alpha)
195 : {
196 0 : CStr v(attr.Value);
197 0 : if (v == "none")
198 0 : p.settings.alpha = ALPHA_NONE;
199 0 : else if (v == "player")
200 0 : p.settings.alpha = ALPHA_PLAYER;
201 0 : else if (v == "transparency")
202 0 : p.settings.alpha = ALPHA_TRANSPARENCY;
203 : else
204 0 : LOGERROR("Invalid attribute value <file alpha='%s'>", v.c_str());
205 : }
206 0 : else if (attr.Name == at_filter)
207 : {
208 0 : CStr v(attr.Value);
209 0 : if (v == "box")
210 0 : p.settings.filter = FILTER_BOX;
211 0 : else if (v == "triangle")
212 0 : p.settings.filter = FILTER_TRIANGLE;
213 0 : else if (v == "kaiser")
214 0 : p.settings.filter = FILTER_KAISER;
215 : else
216 0 : LOGERROR("Invalid attribute value <file filter='%s'>", v.c_str());
217 : }
218 0 : else if (attr.Name == at_kaiserwidth)
219 : {
220 0 : p.settings.kaiserWidth = CStr(attr.Value).ToFloat();
221 : }
222 0 : else if (attr.Name == at_kaiseralpha)
223 : {
224 0 : p.settings.kaiserAlpha = CStr(attr.Value).ToFloat();
225 : }
226 0 : else if (attr.Name == at_kaiserstretch)
227 : {
228 0 : p.settings.kaiserStretch = CStr(attr.Value).ToFloat();
229 : }
230 : else
231 : {
232 0 : LOGERROR("Invalid attribute name <file %s='...'>", XeroFile.GetAttributeString(attr.Name));
233 : }
234 : }
235 :
236 0 : settings->patterns.push_back(p);
237 : }
238 : }
239 :
240 0 : return settings.release();
241 : }
242 :
243 18 : CTextureConverter::Settings CTextureConverter::ComputeSettings(const std::wstring& filename, const std::vector<SettingsFile*>& settingsFiles) const
244 : {
245 : // Set sensible defaults
246 18 : Settings settings;
247 18 : settings.format = FMT_DXT1;
248 18 : settings.mipmap = MIP_TRUE;
249 18 : settings.normal = NORMAL_FALSE;
250 18 : settings.alpha = ALPHA_NONE;
251 18 : settings.filter = FILTER_BOX;
252 18 : settings.kaiserWidth = 3.f;
253 18 : settings.kaiserAlpha = 4.f;
254 18 : settings.kaiserStretch = 1.f;
255 :
256 18 : for (size_t i = 0; i < settingsFiles.size(); ++i)
257 : {
258 0 : for (size_t j = 0; j < settingsFiles[i]->patterns.size(); ++j)
259 : {
260 0 : Match p = settingsFiles[i]->patterns[j];
261 :
262 : // Check that the pattern matches the texture file
263 0 : if (!match_wildcard(filename.c_str(), p.pattern.c_str()))
264 0 : continue;
265 :
266 0 : if (p.settings.format != FMT_UNSPECIFIED)
267 0 : settings.format = p.settings.format;
268 :
269 0 : if (p.settings.mipmap != MIP_UNSPECIFIED)
270 0 : settings.mipmap = p.settings.mipmap;
271 :
272 0 : if (p.settings.normal != NORMAL_UNSPECIFIED)
273 0 : settings.normal = p.settings.normal;
274 :
275 0 : if (p.settings.alpha != ALPHA_UNSPECIFIED)
276 0 : settings.alpha = p.settings.alpha;
277 :
278 0 : if (p.settings.filter != FILTER_UNSPECIFIED)
279 0 : settings.filter = p.settings.filter;
280 :
281 0 : if (p.settings.kaiserWidth != -1.f)
282 0 : settings.kaiserWidth = p.settings.kaiserWidth;
283 :
284 0 : if (p.settings.kaiserAlpha != -1.f)
285 0 : settings.kaiserAlpha = p.settings.kaiserAlpha;
286 :
287 0 : if (p.settings.kaiserStretch != -1.f)
288 0 : settings.kaiserStretch = p.settings.kaiserStretch;
289 : }
290 : }
291 :
292 18 : return settings;
293 : }
294 :
295 11 : CTextureConverter::CTextureConverter(PIVFS vfs, bool highQuality) :
296 11 : m_VFS(vfs), m_HighQuality(highQuality), m_Shutdown(false)
297 : {
298 : #if CONFIG2_NVTT
299 : // Verify that we are running with at least the version we were compiled with,
300 : // to avoid bugs caused by ABI changes
301 11 : ENSURE(nvtt::version() >= NVTT_VERSION);
302 :
303 11 : m_WorkerThread = std::thread(Threading::HandleExceptions<RunThread>::Wrapper, this);
304 :
305 : // Maybe we should share some centralised pool of worker threads?
306 : // For now we'll just stick with a single thread for this specific use.
307 : #endif // CONFIG2_NVTT
308 11 : }
309 :
310 22 : CTextureConverter::~CTextureConverter()
311 : {
312 : #if CONFIG2_NVTT
313 : // Tell the thread to shut down
314 : {
315 22 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
316 11 : m_Shutdown = true;
317 : }
318 :
319 : while (true)
320 : {
321 : // Wake the thread up so that it shutdowns.
322 : // If we send the message once, there is a chance it will be missed,
323 : // so keep sending until shtudown becomes false again, indicating that the thread has shut down.
324 78437 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
325 39224 : m_WorkerCV.notify_all();
326 39224 : if (!m_Shutdown)
327 11 : break;
328 39213 : }
329 :
330 : // Wait for it to shut down cleanly
331 11 : m_WorkerThread.join();
332 : #endif // CONFIG2_NVTT
333 11 : }
334 :
335 7 : bool CTextureConverter::ConvertTexture(const CTexturePtr& texture, const VfsPath& src, const VfsPath& dest, const Settings& settings)
336 : {
337 14 : std::shared_ptr<u8> file;
338 : size_t fileSize;
339 7 : if (m_VFS->LoadFile(src, file, fileSize) < 0)
340 : {
341 0 : LOGERROR("Failed to load texture \"%s\"", src.string8());
342 0 : return false;
343 : }
344 :
345 14 : Tex tex;
346 7 : const Status decodeStatus = tex.decode(file, fileSize);
347 7 : if (decodeStatus != INFO::OK)
348 : {
349 0 : LOGERROR("Failed to decode texture \"%s\" %s", src.string8(), GetStatusAsString(decodeStatus).c_str());
350 0 : return false;
351 : }
352 :
353 7 : if (!is_pow2(tex.m_Width) || !is_pow2(tex.m_Height))
354 : {
355 1 : LOGERROR("Texture to convert \"%s\" should have width and height be power of two: %zux%zu",
356 : src.string8(), tex.m_Width, tex.m_Height);
357 1 : return false;
358 : }
359 :
360 : // Check whether there's any alpha channel
361 6 : bool hasAlpha = ((tex.m_Flags & TEX_ALPHA) != 0);
362 :
363 6 : if (settings.format == FMT_ALPHA)
364 : {
365 : // Convert to uncompressed 8-bit with no mipmaps
366 0 : if (tex.transform_to((tex.m_Flags | TEX_GREY) & ~(TEX_DXT | TEX_MIPMAPS | TEX_ALPHA)) < 0)
367 : {
368 0 : LOGERROR("Failed to transform texture \"%s\"", src.string8());
369 0 : return false;
370 : }
371 : }
372 : else
373 : {
374 : // Convert to uncompressed BGRA with no mipmaps
375 6 : if (tex.transform_to((tex.m_Flags | TEX_BGR | TEX_ALPHA) & ~(TEX_DXT | TEX_MIPMAPS)) < 0)
376 : {
377 0 : LOGERROR("Failed to transform texture \"%s\"", src.string8());
378 0 : return false;
379 : }
380 : }
381 :
382 : // Check if the texture has all alpha=255, so we can automatically
383 : // switch from DXT3/DXT5 to DXT1 with no loss
384 6 : if (hasAlpha)
385 : {
386 4 : hasAlpha = false;
387 4 : u8* data = tex.get_data();
388 4 : for (size_t i = 0; i < tex.m_Width * tex.m_Height; ++i)
389 : {
390 4 : if (data[i*4+3] != 0xFF)
391 : {
392 4 : hasAlpha = true;
393 4 : break;
394 : }
395 : }
396 : }
397 :
398 : #if CONFIG2_NVTT
399 :
400 12 : std::shared_ptr<ConversionRequest> request = std::make_shared<ConversionRequest>();
401 6 : request->dest = dest;
402 6 : request->texture = texture;
403 :
404 : // Apply the chosen settings:
405 :
406 6 : request->inputOptions.setMipmapGeneration(settings.mipmap == MIP_TRUE);
407 :
408 6 : if (settings.alpha == ALPHA_TRANSPARENCY)
409 0 : request->inputOptions.setAlphaMode(nvtt::AlphaMode_Transparency);
410 : else
411 6 : request->inputOptions.setAlphaMode(nvtt::AlphaMode_None);
412 :
413 6 : if (settings.format == FMT_RGBA)
414 : {
415 0 : request->compressionOptions.setFormat(nvtt::Format_RGBA);
416 : // Change the default component order (see tex_dds.cpp decode_pf)
417 0 : request->compressionOptions.setPixelFormat(32, 0xFF, 0xFF00, 0xFF0000, 0xFF000000u);
418 : }
419 6 : else if (settings.format == FMT_ALPHA)
420 : {
421 0 : request->compressionOptions.setFormat(nvtt::Format_RGBA);
422 0 : request->compressionOptions.setPixelFormat(8, 0x00, 0x00, 0x00, 0xFF);
423 : }
424 6 : else if (!hasAlpha)
425 : {
426 : // if no alpha channel then there's no point using DXT3 or DXT5
427 2 : request->compressionOptions.setFormat(nvtt::Format_DXT1);
428 : }
429 4 : else if (settings.format == FMT_DXT1)
430 : {
431 4 : request->compressionOptions.setFormat(nvtt::Format_DXT1a);
432 : }
433 0 : else if (settings.format == FMT_DXT3)
434 : {
435 0 : request->compressionOptions.setFormat(nvtt::Format_DXT3);
436 : }
437 0 : else if (settings.format == FMT_DXT5)
438 : {
439 0 : request->compressionOptions.setFormat(nvtt::Format_DXT5);
440 : }
441 :
442 6 : if (settings.filter == FILTER_BOX)
443 6 : request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Box);
444 0 : else if (settings.filter == FILTER_TRIANGLE)
445 0 : request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Triangle);
446 0 : else if (settings.filter == FILTER_KAISER)
447 0 : request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Kaiser);
448 :
449 6 : if (settings.normal == NORMAL_TRUE)
450 0 : request->inputOptions.setNormalMap(true);
451 :
452 6 : request->inputOptions.setKaiserParameters(settings.kaiserWidth, settings.kaiserAlpha, settings.kaiserStretch);
453 :
454 6 : request->inputOptions.setWrapMode(nvtt::WrapMode_Mirror); // TODO: should this be configurable?
455 :
456 6 : request->compressionOptions.setQuality(m_HighQuality ? nvtt::Quality_Production : nvtt::Quality_Fastest);
457 :
458 : // TODO: normal maps, gamma, etc
459 :
460 : // Load the texture data
461 6 : request->inputOptions.setTextureLayout(nvtt::TextureType_2D, tex.m_Width, tex.m_Height);
462 6 : if (tex.m_Bpp == 32)
463 : {
464 6 : request->inputOptions.setMipmapData(tex.get_data(), tex.m_Width, tex.m_Height);
465 : }
466 : else // bpp == 8
467 : {
468 : // NVTT requires 32-bit input data, so convert
469 0 : const u8* input = tex.get_data();
470 0 : u8* rgba = new u8[tex.m_Width * tex.m_Height * 4];
471 0 : u8* p = rgba;
472 0 : for (size_t i = 0; i < tex.m_Width * tex.m_Height; i++)
473 : {
474 0 : p[0] = p[1] = p[2] = p[3] = *input++;
475 0 : p += 4;
476 : }
477 0 : request->inputOptions.setMipmapData(rgba, tex.m_Width, tex.m_Height);
478 0 : delete[] rgba;
479 : }
480 :
481 : {
482 12 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
483 6 : m_RequestQueue.push_back(request);
484 : }
485 :
486 : // Wake up the worker thread
487 6 : m_WorkerCV.notify_all();
488 :
489 6 : return true;
490 :
491 : #else // CONFIG2_NVTT
492 : LOGERROR("Failed to convert texture \"%s\" (NVTT not available)", src.string8());
493 : return false;
494 : #endif // !CONFIG2_NVTT
495 : }
496 :
497 22 : bool CTextureConverter::Poll(CTexturePtr& texture, VfsPath& dest, bool& ok)
498 : {
499 : #if CONFIG2_NVTT
500 44 : std::shared_ptr<ConversionResult> result;
501 :
502 : // Grab the first result (if any)
503 : {
504 44 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
505 22 : if (!m_ResultQueue.empty())
506 : {
507 6 : result = m_ResultQueue.front();
508 6 : m_ResultQueue.pop_front();
509 : }
510 : }
511 :
512 22 : if (!result)
513 : {
514 : // no work to do
515 16 : return false;
516 : }
517 :
518 6 : if (!result->ret)
519 : {
520 : // conversion had failed
521 0 : ok = false;
522 0 : return true;
523 : }
524 :
525 : // Move output into a correctly-aligned buffer
526 6 : size_t size = result->output.buffer.size();
527 12 : std::shared_ptr<u8> file;
528 6 : AllocateAligned(file, size, maxSectorSize);
529 6 : memcpy(file.get(), &result->output.buffer[0], size);
530 6 : if (m_VFS->CreateFile(result->dest, file, size) < 0)
531 : {
532 : // error writing file
533 0 : ok = false;
534 0 : return true;
535 : }
536 :
537 : // Succeeded in converting texture
538 6 : texture = result->texture;
539 6 : dest = result->dest;
540 6 : ok = true;
541 6 : return true;
542 :
543 : #else // CONFIG2_NVTT
544 : return false;
545 : #endif // !CONFIG2_NVTT
546 : }
547 :
548 23 : bool CTextureConverter::IsBusy()
549 : {
550 : #if CONFIG2_NVTT
551 46 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
552 46 : return !m_RequestQueue.empty();
553 : #else // CONFIG2_NVTT
554 : return false;
555 : #endif // !CONFIG2_NVTT
556 : }
557 :
558 11 : void CTextureConverter::RunThread(CTextureConverter* textureConverter)
559 : {
560 : #if CONFIG2_NVTT
561 11 : debug_SetThreadName("TextureConverter");
562 11 : g_Profiler2.RegisterCurrentThread("texconv");
563 : // Wait until the main thread wakes us up
564 : while (true)
565 : {
566 : // We may have several textures in the incoming queue, process them all before going back to sleep.
567 17 : if (!textureConverter->IsBusy()) {
568 34 : std::unique_lock<std::mutex> wait_lock(textureConverter->m_WorkerMutex);
569 : // Use the no-condition variant because spurious wake-ups don't matter that much here.
570 17 : textureConverter->m_WorkerCV.wait(wait_lock);
571 : }
572 :
573 17 : g_Profiler2.RecordSyncMarker();
574 17 : PROFILE2_EVENT("wakeup");
575 :
576 23 : std::shared_ptr<ConversionRequest> request;
577 :
578 : {
579 23 : std::lock_guard<std::mutex> wait_lock(textureConverter->m_WorkerMutex);
580 17 : if (textureConverter->m_Shutdown)
581 11 : break;
582 : // If we weren't woken up for shutdown, we must have been woken up for
583 : // a new request, so grab it from the queue
584 6 : request = textureConverter->m_RequestQueue.front();
585 6 : textureConverter->m_RequestQueue.pop_front();
586 : }
587 :
588 : // Set up the result object
589 12 : std::shared_ptr<ConversionResult> result = std::make_shared<ConversionResult>();
590 6 : result->dest = request->dest;
591 6 : result->texture = request->texture;
592 :
593 6 : request->outputOptions.setOutputHandler(&result->output);
594 :
595 : // TIMER(L"TextureConverter compress");
596 :
597 : {
598 12 : PROFILE2("compress");
599 :
600 : // Perform the compression
601 12 : nvtt::Compressor compressor;
602 6 : result->ret = compressor.process(request->inputOptions, request->compressionOptions, request->outputOptions);
603 : }
604 :
605 : // Push the result onto the queue
606 12 : std::lock_guard<std::mutex> wait_lock(textureConverter->m_WorkerMutex);
607 6 : textureConverter->m_ResultQueue.push_back(result);
608 6 : }
609 :
610 22 : std::lock_guard<std::mutex> wait_lock(textureConverter->m_WorkerMutex);
611 11 : textureConverter->m_Shutdown = false;
612 : #endif // CONFIG2_NVTT
613 14 : }
|