Pyrogenesis  13997
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
TextureConverter.cpp
Go to the documentation of this file.
1 /* Copyright (C) 2010 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/regex.h"
23 #include "lib/timer.h"
25 #include "lib/tex/tex.h"
26 #include "maths/MD5.h"
27 #include "ps/CLogger.h"
28 #include "ps/CStr.h"
29 #include "ps/Profiler2.h"
30 #include "ps/XML/Xeromyces.h"
31 
32 #if CONFIG2_NVTT
33 
34 #include "nvtt/nvtt.h"
35 
36 /**
37  * Output handler to collect NVTT's output into a simplistic buffer.
38  * WARNING: Used in the worker thread - must be thread-safe.
39  */
40 struct BufferOutputHandler : public nvtt::OutputHandler
41 {
42  std::vector<u8> buffer;
43 
44  virtual void beginImage(int UNUSED(size), int UNUSED(width), int UNUSED(height), int UNUSED(depth), int UNUSED(face), int UNUSED(miplevel))
45  {
46  }
47 
48  virtual bool writeData(const void* data, int size)
49  {
50  size_t off = buffer.size();
51  buffer.resize(off + size);
52  memcpy(&buffer[off], data, size);
53  return true;
54  }
55 };
56 
57 /**
58  * Request for worker thread to process.
59  */
61 {
64  nvtt::InputOptions inputOptions;
65  nvtt::CompressionOptions compressionOptions;
66  nvtt::OutputOptions outputOptions;
67  bool isDXT1a; // see comment in RunThread
68 };
69 
70 /**
71  * Result from worker thread.
72  */
74 {
78  bool ret; // true if the conversion succeeded
79 };
80 
81 #endif // CONFIG2_NVTT
82 
84 {
85  hash.Update((const u8*)&format, sizeof(format));
86  hash.Update((const u8*)&mipmap, sizeof(mipmap));
87  hash.Update((const u8*)&normal, sizeof(normal));
88  hash.Update((const u8*)&alpha, sizeof(alpha));
89  hash.Update((const u8*)&filter, sizeof(filter));
90  hash.Update((const u8*)&kaiserWidth, sizeof(kaiserWidth));
91  hash.Update((const u8*)&kaiserAlpha, sizeof(kaiserAlpha));
92  hash.Update((const u8*)&kaiserStretch, sizeof(kaiserStretch));
93 }
94 
96 {
97  CXeromyces XeroFile;
98  if (XeroFile.Load(m_VFS, path) != PSRETURN_OK)
99  return NULL;
100 
101  // Define all the elements used in the XML file
102  #define EL(x) int el_##x = XeroFile.GetElementID(#x)
103  #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
104  EL(textures);
105  EL(file);
106  AT(pattern);
107  AT(format);
108  AT(mipmap);
109  AT(normal);
110  AT(alpha);
111  AT(filter);
112  AT(kaiserwidth);
113  AT(kaiseralpha);
114  AT(kaiserstretch);
115  #undef AT
116  #undef EL
117 
118  XMBElement root = XeroFile.GetRoot();
119 
120  if (root.GetNodeName() != el_textures)
121  {
122  LOGERROR(L"Invalid texture settings file \"%ls\" (unrecognised root element)", path.string().c_str());
123  return NULL;
124  }
125 
126  std::auto_ptr<SettingsFile> settings(new SettingsFile());
127 
128  XERO_ITER_EL(root, child)
129  {
130  if (child.GetNodeName() == el_file)
131  {
132  Match p;
133 
134  XERO_ITER_ATTR(child, attr)
135  {
136  if (attr.Name == at_pattern)
137  {
138  p.pattern = attr.Value.FromUTF8();
139  }
140  else if (attr.Name == at_format)
141  {
142  CStr v(attr.Value);
143  if (v == "dxt1")
145  else if (v == "dxt3")
147  else if (v == "dxt5")
149  else if (v == "rgba")
151  else
152  LOGERROR(L"Invalid attribute value <file format='%hs'>", v.c_str());
153  }
154  else if (attr.Name == at_mipmap)
155  {
156  CStr v(attr.Value);
157  if (v == "true")
159  else if (v == "false")
161  else
162  LOGERROR(L"Invalid attribute value <file mipmap='%hs'>", v.c_str());
163  }
164  else if (attr.Name == at_normal)
165  {
166  CStr v(attr.Value);
167  if (v == "true")
169  else if (v == "false")
171  else
172  LOGERROR(L"Invalid attribute value <file normal='%hs'>", v.c_str());
173  }
174  else if (attr.Name == at_alpha)
175  {
176  CStr v(attr.Value);
177  if (v == "none")
179  else if (v == "player")
181  else if (v == "transparency")
183  else
184  LOGERROR(L"Invalid attribute value <file alpha='%hs'>", v.c_str());
185  }
186  else if (attr.Name == at_filter)
187  {
188  CStr v(attr.Value);
189  if (v == "box")
191  else if (v == "triangle")
193  else if (v == "kaiser")
195  else
196  LOGERROR(L"Invalid attribute value <file filter='%hs'>", v.c_str());
197  }
198  else if (attr.Name == at_kaiserwidth)
199  {
200  p.settings.kaiserWidth = CStr(attr.Value).ToFloat();
201  }
202  else if (attr.Name == at_kaiseralpha)
203  {
204  p.settings.kaiserAlpha = CStr(attr.Value).ToFloat();
205  }
206  else if (attr.Name == at_kaiserstretch)
207  {
208  p.settings.kaiserStretch = CStr(attr.Value).ToFloat();
209  }
210  else
211  {
212  LOGERROR(L"Invalid attribute name <file %hs='...'>", XeroFile.GetAttributeString(attr.Name).c_str());
213  }
214  }
215 
216  settings->patterns.push_back(p);
217  }
218  }
219 
220  return settings.release();
221 }
222 
223 CTextureConverter::Settings CTextureConverter::ComputeSettings(const std::wstring& filename, const std::vector<SettingsFile*>& settingsFiles) const
224 {
225  // Set sensible defaults
226  Settings settings;
227  settings.format = FMT_DXT1;
228  settings.mipmap = MIP_TRUE;
229  settings.normal = NORMAL_FALSE;
230  settings.alpha = ALPHA_NONE;
231  settings.filter = FILTER_BOX;
232  settings.kaiserWidth = 3.f;
233  settings.kaiserAlpha = 4.f;
234  settings.kaiserStretch = 1.f;
235 
236  for (size_t i = 0; i < settingsFiles.size(); ++i)
237  {
238  for (size_t j = 0; j < settingsFiles[i]->patterns.size(); ++j)
239  {
240  Match p = settingsFiles[i]->patterns[j];
241 
242  // Check that the pattern matches the texture file
243  if (!match_wildcard(filename.c_str(), p.pattern.c_str()))
244  continue;
245 
247  settings.format = p.settings.format;
248 
250  settings.mipmap = p.settings.mipmap;
251 
253  settings.normal = p.settings.normal;
254 
256  settings.alpha = p.settings.alpha;
257 
259  settings.filter = p.settings.filter;
260 
261  if (p.settings.kaiserWidth != -1.f)
262  settings.kaiserWidth = p.settings.kaiserWidth;
263 
264  if (p.settings.kaiserAlpha != -1.f)
265  settings.kaiserAlpha = p.settings.kaiserAlpha;
266 
267  if (p.settings.kaiserStretch != -1.f)
268  settings.kaiserStretch = p.settings.kaiserStretch;
269  }
270  }
271 
272  return settings;
273 }
274 
276  m_VFS(vfs), m_HighQuality(highQuality), m_Shutdown(false)
277 {
278  // Verify that we are running with at least the version we were compiled with,
279  // to avoid bugs caused by ABI changes
280 #if CONFIG2_NVTT
281  ENSURE(nvtt::version() >= NVTT_VERSION);
282 #endif
283 
284  // Set up the worker thread:
285 
286  int ret;
287 
288  // Use SDL semaphores since OS X doesn't implement sem_init
291 
292  ret = pthread_mutex_init(&m_WorkerMutex, NULL);
293  ENSURE(ret == 0);
294 
295  ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
296  ENSURE(ret == 0);
297 
298  // Maybe we should share some centralised pool of worker threads?
299  // For now we'll just stick with a single thread for this specific use.
300 }
301 
303 {
304  // Tell the thread to shut down
306  m_Shutdown = true;
308 
309  // Wake it up so it sees the notification
311 
312  // Wait for it to shut down cleanly
314 
315  // Clean up resources
318 }
319 
320 bool CTextureConverter::ConvertTexture(const CTexturePtr& texture, const VfsPath& src, const VfsPath& dest, const Settings& settings)
321 {
322  shared_ptr<u8> file;
323  size_t fileSize;
324  if (m_VFS->LoadFile(src, file, fileSize) < 0)
325  {
326  LOGERROR(L"Failed to load texture \"%ls\"", src.string().c_str());
327  return false;
328  }
329 
330  Tex tex;
331  if (tex_decode(file, fileSize, &tex) < 0)
332  {
333  LOGERROR(L"Failed to decode texture \"%ls\"", src.string().c_str());
334  return false;
335  }
336 
337  // Check whether there's any alpha channel
338  bool hasAlpha = ((tex.flags & TEX_ALPHA) != 0);
339 
340  // TODO: grayscale images will fail on some systems
341  // see http://trac.wildfiregames.com/ticket/1640
342  if (tex.flags & TEX_GREY)
343  {
344  LOGERROR(L"Failed to convert grayscale texture \"%ls\" - only RGB textures are currently supported", src.string().c_str());
345  return false;
346  }
347 
348  // Convert to uncompressed BGRA with no mipmaps
349  if (tex_transform_to(&tex, (tex.flags | TEX_BGR | TEX_ALPHA) & ~(TEX_DXT | TEX_MIPMAPS)) < 0)
350  {
351  LOGERROR(L"Failed to transform texture \"%ls\"", src.string().c_str());
352  tex_free(&tex);
353  return false;
354  }
355 
356  // Check if the texture has all alpha=255, so we can automatically
357  // switch from DXT3/DXT5 to DXT1 with no loss
358  if (hasAlpha)
359  {
360  hasAlpha = false;
361  u8* data = tex_get_data(&tex);
362  for (size_t i = 0; i < tex.w * tex.h; ++i)
363  {
364  if (data[i*4+3] != 0xFF)
365  {
366  hasAlpha = true;
367  break;
368  }
369  }
370  }
371 
372 #if CONFIG2_NVTT
373 
374  shared_ptr<ConversionRequest> request(new ConversionRequest);
375  request->dest = dest;
376  request->texture = texture;
377 
378  // Apply the chosen settings:
379 
380  request->inputOptions.setMipmapGeneration(settings.mipmap == MIP_TRUE);
381 
382  if (settings.alpha == ALPHA_TRANSPARENCY)
383  request->inputOptions.setAlphaMode(nvtt::AlphaMode_Transparency);
384  else
385  request->inputOptions.setAlphaMode(nvtt::AlphaMode_None);
386 
387  request->isDXT1a = false;
388 
389  if (settings.format == FMT_RGBA)
390  {
391  request->compressionOptions.setFormat(nvtt::Format_RGBA);
392  // Change the default component order (see tex_dds.cpp decode_pf)
393  request->compressionOptions.setPixelFormat(32, 0xFF, 0xFF00, 0xFF0000, 0xFF000000u);
394  }
395  else if (!hasAlpha)
396  {
397  // if no alpha channel then there's no point using DXT3 or DXT5
398  request->compressionOptions.setFormat(nvtt::Format_DXT1);
399  }
400  else if (settings.format == FMT_DXT1)
401  {
402  request->compressionOptions.setFormat(nvtt::Format_DXT1a);
403  request->isDXT1a = true;
404  }
405  else if (settings.format == FMT_DXT3)
406  {
407  request->compressionOptions.setFormat(nvtt::Format_DXT3);
408  }
409  else if (settings.format == FMT_DXT5)
410  {
411  request->compressionOptions.setFormat(nvtt::Format_DXT5);
412  }
413 
414  if (settings.filter == FILTER_BOX)
415  request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Box);
416  else if (settings.filter == FILTER_TRIANGLE)
417  request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Triangle);
418  else if (settings.filter == FILTER_KAISER)
419  request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Kaiser);
420 
421  if (settings.normal == NORMAL_TRUE)
422  request->inputOptions.setNormalMap(true);
423 
424  request->inputOptions.setKaiserParameters(settings.kaiserWidth, settings.kaiserAlpha, settings.kaiserStretch);
425 
426  request->inputOptions.setWrapMode(nvtt::WrapMode_Mirror); // TODO: should this be configurable?
427 
428  request->compressionOptions.setQuality(m_HighQuality ? nvtt::Quality_Production : nvtt::Quality_Fastest);
429 
430  // TODO: normal maps, gamma, etc
431 
432  // Load the texture data
433  request->inputOptions.setTextureLayout(nvtt::TextureType_2D, tex.w, tex.h);
434  request->inputOptions.setMipmapData(tex_get_data(&tex), tex.w, tex.h);
435 
436  // NVTT copies the texture data so we can free it now
437  tex_free(&tex);
438 
440  m_RequestQueue.push_back(request);
442 
443  // Wake up the worker thread
445 
446  return true;
447 
448 #else
449  LOGERROR(L"Failed to convert texture \"%ls\" (NVTT not available)", src.string().c_str());
450  return false;
451 #endif
452 }
453 
454 bool CTextureConverter::Poll(CTexturePtr& texture, VfsPath& dest, bool& ok)
455 {
456 #if CONFIG2_NVTT
457  shared_ptr<ConversionResult> result;
458 
459  // Grab the first result (if any)
461  if (!m_ResultQueue.empty())
462  {
463  result = m_ResultQueue.front();
464  m_ResultQueue.pop_front();
465  }
467 
468  if (!result)
469  {
470  // no work to do
471  return false;
472  }
473 
474  if (!result->ret)
475  {
476  // conversion had failed
477  ok = false;
478  return true;
479  }
480 
481  // Move output into a correctly-aligned buffer
482  size_t size = result->output.buffer.size();
483  shared_ptr<u8> file;
484  AllocateAligned(file, size, maxSectorSize);
485  memcpy(file.get(), &result->output.buffer[0], size);
486  if (m_VFS->CreateFile(result->dest, file, size) < 0)
487  {
488  // error writing file
489  ok = false;
490  return true;
491  }
492 
493  // Succeeded in converting texture
494  texture = result->texture;
495  dest = result->dest;
496  ok = true;
497  return true;
498 
499 #else // #if CONFIG2_NVTT
500  return false;
501 #endif
502 }
503 
505 {
507  bool busy = !m_RequestQueue.empty();
509 
510  return busy;
511 }
512 
514 {
515  debug_SetThreadName("TextureConverter");
517 
518  CTextureConverter* textureConverter = static_cast<CTextureConverter*>(data);
519 
520 #if CONFIG2_NVTT
521 
522  // Wait until the main thread wakes us up
523  while (SDL_SemWait(textureConverter->m_WorkerSem) == 0)
524  {
526  PROFILE2_EVENT("wakeup");
527 
528  pthread_mutex_lock(&textureConverter->m_WorkerMutex);
529  if (textureConverter->m_Shutdown)
530  {
531  pthread_mutex_unlock(&textureConverter->m_WorkerMutex);
532  break;
533  }
534  // If we weren't woken up for shutdown, we must have been woken up for
535  // a new request, so grab it from the queue
536  shared_ptr<ConversionRequest> request = textureConverter->m_RequestQueue.front();
537  textureConverter->m_RequestQueue.pop_front();
538  pthread_mutex_unlock(&textureConverter->m_WorkerMutex);
539 
540  // Set up the result object
541  shared_ptr<ConversionResult> result(new ConversionResult());
542  result->dest = request->dest;
543  result->texture = request->texture;
544 
545  request->outputOptions.setOutputHandler(&result->output);
546 
547 // TIMER(L"TextureConverter compress");
548 
549  {
550  PROFILE2("compress");
551 
552  // Perform the compression
553  nvtt::Compressor compressor;
554  result->ret = compressor.process(request->inputOptions, request->compressionOptions, request->outputOptions);
555  }
556 
557  // Ugly hack: NVTT 2.0 doesn't set DDPF_ALPHAPIXELS for DXT1a, so we can't
558  // distinguish it from DXT1. (It's fixed in trunk by
559  // http://code.google.com/p/nvidia-texture-tools/source/detail?r=924&path=/trunk).
560  // Rather than using a trunk NVTT (unstable, makes packaging harder)
561  // or patching our copy (makes packaging harder), we'll just manually
562  // set the flag here.
563  if (request->isDXT1a && result->ret && result->output.buffer.size() > 80)
564  result->output.buffer[80] |= 1; // DDPF_ALPHAPIXELS in DDS_PIXELFORMAT.dwFlags
565 
566  // Push the result onto the queue
567  pthread_mutex_lock(&textureConverter->m_WorkerMutex);
568  textureConverter->m_ResultQueue.push_back(result);
569  pthread_mutex_unlock(&textureConverter->m_WorkerMutex);
570  }
571 
572 #endif
573 
574  return NULL;
575 }
#define PROFILE2_EVENT(name)
Record the named event at the current time.
Definition: Profiler2.h:453
#define u8
Definition: types.h:39
#define UNUSED(param)
mark a function parameter as unused and avoid the corresponding compiler warning. ...
bool Poll(CTexturePtr &texture, VfsPath &dest, bool &ok)
Returns the result of a successful ConvertTexture call.
std::deque< shared_ptr< ConversionResult > > m_ResultQueue
PSRETURN Load(const PIVFS &vfs, const VfsPath &filename)
Load from an XML file (with invisible XMB caching).
Definition: Xeromyces.cpp:65
#define LOGERROR
Definition: CLogger.h:35
#define XERO_ITER_ATTR(parent_element, attribute)
Definition: Xeromyces.h:99
Status tex_transform_to(Tex *t, size_t new_flags)
Change &lt;t&gt;&#39;s pixel format (2nd version) (note: this is equivalent to tex_transform(t, t-&gt;flags^new_flags).
Definition: tex.cpp:494
const PSRETURN PSRETURN_OK
Definition: Errors.h:103
std::vector< u8 > buffer
static const uintptr_t maxSectorSize
Definition: alignment.h:82
shared_ptr< IVFS > PIVFS
Definition: vfs.h:226
flags &amp; TEX_DXT is a field indicating compression.
Definition: tex.h:149
mask
Definition: tex.h:199
indicates the image contains an alpha channel.
Definition: tex.h:171
SettingsFile * LoadSettings(const VfsPath &path) const
Load a texture conversion settings XML file.
indicates the image is 8bpp greyscale.
Definition: tex.h:178
Texture conversion settings.
#define XERO_ITER_EL(parent_element, child_element)
Definition: Xeromyces.h:91
int match_wildcard(const wchar_t *s, const wchar_t *w)
see if string matches pattern.
Definition: regex.cpp:28
size_t h
Definition: tex.h:230
pthread_mutex_t m_WorkerMutex
void Hash(MD5 &hash)
Append this object&#39;s state to the given hash.
indicates B and R pixel components are exchanged.
Definition: tex.h:163
static void * RunThread(void *data)
virtual void beginImage(int size, int width, int height, int depth, int face, int miplevel)
#define AT(x)
LIB_API void debug_SetThreadName(const char *name)
inform the debugger of the current thread&#39;s name.
Definition: bdbg.cpp:126
void SDL_DestroySemaphore(SDL_sem *sem)
Definition: wsdl.cpp:1420
#define ENSURE(expr)
ensure the expression &lt;expr&gt; evaluates to non-zero.
Definition: debug.h:282
#define PROFILE2(region)
Starts timing from now until the end of the current scope.
Definition: Profiler2.h:446
void Update(const u8 *data, size_t len)
Definition: MD5.h:34
int pthread_create(pthread_t *thread_id, const void *attr, void *(*func)(void *), void *arg)
Definition: wpthread.cpp:636
Representation of &lt;File&gt; line from settings XML file.
MD5 hashing algorithm.
Definition: MD5.h:27
CProfiler2 g_Profiler2
Definition: Profiler2.cpp:35
Definition: path.h:75
New profiler (complementing the older CProfileManager)
void RegisterCurrentThread(const std::string &name)
Call in any thread to enable the profiler in that thread.
Definition: Profiler2.cpp:241
const String & string() const
Definition: path.h:123
u8 * tex_get_data(const Tex *t)
rationale: since Tex is a struct, its fields are accessible to callers.
Definition: tex.cpp:629
int pthread_mutex_lock(pthread_mutex_t *m)
Definition: wpthread.cpp:329
nvtt::CompressionOptions compressionOptions
int pthread_mutex_init(pthread_mutex_t *m, const pthread_mutexattr_t *)
Definition: wpthread.cpp:323
Representation of settings XML file.
stores all data describing an image.
Definition: tex.h:210
#define EL(x)
bool IsBusy()
Returns whether there is currently a queued request from ConvertTexture().
SDL_sem * SDL_CreateSemaphore(int cnt)
Definition: wsdl.cpp:1414
static Status AllocateAligned(shared_ptr< T > &p, size_t size, size_t alignment=cacheLineSize)
Definition: shared_ptr.h:66
XMBElement GetRoot() const
Definition: XeroXMB.cpp:84
size_t w
Definition: tex.h:229
int pthread_mutex_unlock(pthread_mutex_t *m)
Definition: wpthread.cpp:347
Texture conversion helper class.
Result from worker thread.
int pthread_join(pthread_t thread, void **value_ptr)
Definition: wpthread.cpp:679
~CTextureConverter()
Destroy texture converter and wait to shut down worker thread.
std::string GetAttributeString(const int ID) const
Definition: XeroXMB.cpp:156
int pthread_mutex_destroy(pthread_mutex_t *m)
Definition: wpthread.cpp:312
int SDL_SemPost(SDL_sem *sem)
Definition: wsdl.cpp:1426
virtual bool writeData(const void *data, int size)
void RecordSyncMarker()
Non-main threads should call this occasionally, especially if it&#39;s been a long time since their last ...
Definition: Profiler2.h:296
Request for worker thread to process.
Output handler to collect NVTT&#39;s output into a simplistic buffer.
int SDL_SemWait(SDL_sem *sem)
Definition: wsdl.cpp:1432
size_t flags
see TexFlags and &quot;Format Conversion&quot; in docs.
Definition: tex.h:234
Status tex_decode(const shared_ptr< u8 > &data, size_t dataSize, Tex *t)
decode an in-memory texture file into texture object.
Definition: tex.cpp:717
std::deque< shared_ptr< ConversionRequest > > m_RequestQueue
void tex_free(Tex *t)
free all resources associated with the image and make further use of it impossible.
Definition: tex.cpp:610
bool ConvertTexture(const CTexturePtr &texture, const VfsPath &src, const VfsPath &dest, const Settings &settings)
Begin converting a texture, using the given settings.
Settings ComputeSettings(const std::wstring &filename, const std::vector< SettingsFile * > &settingsFiles) const
Match a sequence of settings files against a given texture filename, and return the resulting setting...
CTextureConverter(PIVFS vfs, bool highQuality)
Construct texture converter, for use with files in the given vfs.
int GetNodeName() const
Definition: XeroXMB.cpp:166
shared_ptr< CTexture > CTexturePtr
Definition: Texture.h:22