Browse Source

More work on the image importer

Panagiotis Christopoulos Charitos 4 years ago
parent
commit
c23f4410b0

+ 1 - 1
AnKi/Config.h.cmake

@@ -157,7 +157,7 @@
 #	define ANKI_HOT __attribute__ ((hot))
 #	define ANKI_UNREACHABLE() __builtin_unreachable()
 #	define ANKI_PREFETCH_MEMORY(addr) __builtin_prefetch(addr)
-#	define ANKI_CHECK_FORMAT(fmtArgIdx, firstArgIdx) __attribute__ ((format (printf, fmtArgIdx + 1, firstArgIdx + 1)))
+#	define ANKI_CHECK_FORMAT(fmtArgIdx, firstArgIdx) __attribute__((format(printf, fmtArgIdx + 1, firstArgIdx + 1))) // On methods need to include "this"
 #else
 #	define ANKI_LIKELY(x) ((x) == 1)
 #	define ANKI_UNLIKELY(x) ((x) == 1)

+ 2 - 2
AnKi/Gr/Common.h

@@ -400,12 +400,12 @@ inline U32 computeMaxMipmapCount2d(U32 w, U32 h, U32 minSizeOfLastMip = 1)
 }
 
 /// Compute max number of mipmaps for a 3D texture.
-inline U32 computeMaxMipmapCount3d(U32 w, U32 h, U32 d)
+inline U32 computeMaxMipmapCount3d(U32 w, U32 h, U32 d, U32 minSizeOfLastMip = 1)
 {
 	U32 s = (w < h) ? w : h;
 	s = (s < d) ? s : d;
 	U32 count = 0;
-	while(s)
+	while(s >= minSizeOfLastMip)
 	{
 		s /= 2;
 		++count;

+ 362 - 28
AnKi/Importer/ImageImporter.cpp

@@ -4,69 +4,403 @@
 // http://www.anki3d.org/LICENSE
 
 #include <AnKi/Importer/ImageImporter.h>
-
-#define STBI_ASSERT(x) ANKI_ASSERT(x)
-#if ANKI_COMPILER_GCC_COMPATIBLE
-#	pragma GCC diagnostic push
-#	pragma GCC diagnostic ignored "-Wfloat-conversion"
-#	pragma GCC diagnostic ignored "-Wconversion"
-#	pragma GCC diagnostic ignored "-Wtype-limits"
-#endif
-#include <Stb/stb_image.h>
-#if ANKI_COMPILER_GCC_COMPATIBLE
-#	pragma GCC diagnostic pop
-#endif
+#include <AnKi/Gr/Common.h>
+#include <AnKi/Resource/Stb.h>
 
 namespace anki
 {
 
-static Bool checkConfig(const ImageImporterConfig& config)
+namespace
+{
+
+class SurfaceOrVolumeData
+{
+public:
+	DynamicArrayAuto<U8, PtrSize> m_pixels;
+
+	SurfaceOrVolumeData(GenericMemoryPoolAllocator<U8> alloc)
+		: m_pixels(alloc)
+	{
+	}
+};
+
+class Mipmap
+{
+public:
+	DynamicArrayAuto<SurfaceOrVolumeData> m_surfacesOrVolume;
+
+	Mipmap(GenericMemoryPoolAllocator<U8> alloc)
+		: m_surfacesOrVolume(alloc)
+	{
+	}
+};
+
+/// Image importer context.
+class ImageImporterContext
+{
+public:
+	DynamicArrayAuto<Mipmap> m_mipmaps;
+	U32 m_width = 0;
+	U32 m_height = 0;
+	U32 m_depth = 0;
+	U32 m_faceCount = 0;
+	U32 m_layerCount = 0;
+	U32 m_channelCount = 0;
+	U32 m_pixelSize = 0;
+
+	ImageImporterContext(GenericMemoryPoolAllocator<U8> alloc)
+		: m_mipmaps(alloc)
+	{
+	}
+
+	GenericMemoryPoolAllocator<U8> getAllocator() const
+	{
+		return m_mipmaps.getAllocator();
+	}
+};
+
+} // namespace
+
+static ANKI_USE_RESULT Error checkConfig(const ImageImporterConfig& config)
 {
-#define ANKI_CFG_ASSERT(x) \
+#define ANKI_CFG_ASSERT(x, message) \
 	do \
 	{ \
 		if(!(x)) \
 		{ \
-			return false; \
+			ANKI_IMPORTER_LOGE(message); \
+			return Error::USER_DATA; \
 		} \
 	} while(false)
 
 	// Filenames
-	ANKI_CFG_ASSERT(config.m_outFilename.getLength() > 0);
+	ANKI_CFG_ASSERT(config.m_outFilename.getLength() > 0, "Empty output filename");
 
 	for(CString in : config.m_inputFilenames)
 	{
-		ANKI_CFG_ASSERT(in.getLength() > 0);
+		ANKI_CFG_ASSERT(in.getLength() > 0, "Empty input filename");
 	}
 
 	// Type
-	ANKI_CFG_ASSERT(config.m_type != ImageBinaryType::NONE);
-	ANKI_CFG_ASSERT(config.m_inputFilenames.getSize() == 1 || config.m_type != ImageBinaryType::_2D);
-	ANKI_CFG_ASSERT(config.m_inputFilenames.getSize() != 1 || config.m_type != ImageBinaryType::_2D_ARRAY);
-	ANKI_CFG_ASSERT(config.m_inputFilenames.getSize() != 1 || config.m_type != ImageBinaryType::_3D);
-	ANKI_CFG_ASSERT(config.m_inputFilenames.getSize() != 6 || config.m_type != ImageBinaryType::CUBE);
+	ANKI_CFG_ASSERT(config.m_type != ImageBinaryType::NONE, "Wrong image type");
+	ANKI_CFG_ASSERT(config.m_inputFilenames.getSize() == 1 || config.m_type != ImageBinaryType::_2D,
+					"2D images require only one input image");
+	ANKI_CFG_ASSERT(config.m_inputFilenames.getSize() != 1 || config.m_type != ImageBinaryType::_2D_ARRAY,
+					"2D array images require more than one input image");
+	ANKI_CFG_ASSERT(config.m_inputFilenames.getSize() != 1 || config.m_type != ImageBinaryType::_3D,
+					"3D images require more than one input image");
+	ANKI_CFG_ASSERT(config.m_inputFilenames.getSize() != 6 || config.m_type != ImageBinaryType::CUBE,
+					"Cube images require 6 input images");
 
 	// Compressions
-	ANKI_CFG_ASSERT(config.m_compressions != ImageBinaryDataCompression::NONE);
+	ANKI_CFG_ASSERT(config.m_compressions != ImageBinaryDataCompression::NONE, "Missing output compressions");
+	ANKI_CFG_ASSERT(config.m_compressions == ImageBinaryDataCompression::RAW || config.m_type != ImageBinaryType::_3D,
+					"Can't compress 3D textures");
 
 	// Mip size
-	ANKI_CFG_ASSERT(config.m_minMipmapDimension > 4);
+	ANKI_CFG_ASSERT(config.m_minMipmapDimension > 4, "Mimpap min dimension can be less than 4");
 
 #undef ANKI_CFG_ASSERT
-	return true;
+	return Error::NONE;
 }
 
-static Error importImageInternal(const ImageImporterConfig& config)
+static ANKI_USE_RESULT Error checkInputImages(const ImageImporterConfig& config, U32& width, U32& height,
+											  U32& channelCount)
 {
-	if(!checkConfig(config))
+	width = 0;
+	height = 0;
+	channelCount = 0;
+
+	for(U32 i = 0; i < config.m_inputFilenames.getSize(); ++i)
 	{
-		ANKI_IMPORTER_LOGE("Config parameters are wrong");
+		I32 iwidth, iheight, ichannelCount;
+		const I ok = stbi_info(config.m_inputFilenames[i].cstr(), &iwidth, &iheight, &ichannelCount);
+		if(!ok)
+		{
+			ANKI_IMPORTER_LOGE("STB failed to load file: %s", config.m_inputFilenames[i].cstr());
+			return Error::FUNCTION_FAILED;
+		}
+
+		if(width == 0)
+		{
+			width = U32(iwidth);
+			height = U32(iheight);
+			channelCount = U32(ichannelCount);
+		}
+		else if(width != U32(iwidth) || height != U32(iheight) || channelCount != U32(ichannelCount))
+		{
+			ANKI_IMPORTER_LOGE("Input image doesn't match previous input images: %s",
+							   config.m_inputFilenames[i].cstr());
+			return Error::USER_DATA;
+		}
+	}
+
+	ANKI_ASSERT(width > 0 && height > 0 && channelCount > 0);
+	if(!isPowerOfTwo(width) || !isPowerOfTwo(height))
+	{
+		ANKI_IMPORTER_LOGE("Only power of two images are accepted");
 		return Error::USER_DATA;
 	}
 
 	return Error::NONE;
 }
 
+static ANKI_USE_RESULT Error loadFirstMipmap(const ImageImporterConfig& config, ImageImporterContext& ctx)
+{
+	GenericMemoryPoolAllocator<U8> alloc = ctx.getAllocator();
+
+	ctx.m_mipmaps.emplaceBack(alloc);
+	Mipmap& mip0 = ctx.m_mipmaps[0];
+
+	if(ctx.m_depth > 1)
+	{
+		mip0.m_surfacesOrVolume.create(1, alloc);
+		mip0.m_surfacesOrVolume[0].m_pixels.create(ctx.m_pixelSize * ctx.m_width * ctx.m_height * ctx.m_depth);
+	}
+	else
+	{
+		mip0.m_surfacesOrVolume.create(ctx.m_faceCount * ctx.m_layerCount, alloc);
+
+		for(U32 f = 0; f < ctx.m_faceCount; ++f)
+		{
+			for(U32 l = 0; l < ctx.m_layerCount; ++l)
+			{
+				mip0.m_surfacesOrVolume[l * ctx.m_faceCount + f].m_pixels.create(ctx.m_pixelSize * ctx.m_width
+																				 * ctx.m_height);
+			}
+		}
+	}
+
+	for(U32 i = 0; i < config.m_inputFilenames.getSize(); ++i)
+	{
+		I32 width, height, c;
+		void* data = stbi_load(config.m_inputFilenames[i].cstr(), &width, &height, &c, ctx.m_channelCount);
+		ANKI_ASSERT(U32(c) == ctx.m_channelCount);
+		if(!data)
+		{
+			ANKI_IMPORTER_LOGE("STB load failed: %s", config.m_inputFilenames[i].cstr());
+			return Error::FUNCTION_FAILED;
+		}
+
+		const PtrSize dataSize = PtrSize(ctx.m_width) * ctx.m_height * ctx.m_pixelSize;
+
+		if(ctx.m_depth > 1)
+		{
+			memcpy(mip0.m_surfacesOrVolume.getBegin() + i * dataSize, data, dataSize);
+		}
+		else
+		{
+			for(U32 l = 0; l < ctx.m_layerCount; ++l)
+			{
+				for(U32 f = 0; f < ctx.m_faceCount; ++f)
+				{
+					memcpy(mip0.m_surfacesOrVolume[l * ctx.m_faceCount + f].m_pixels.getBegin(), data, dataSize);
+				}
+			}
+		}
+
+		stbi_image_free(data);
+	}
+
+	return Error::NONE;
+}
+
+template<U32 TCHANNEL_COUNT>
+static void generateSurfaceMipmap(const void* input, PtrSize inputBufferSize, U32 inWidth, U32 inHeight,
+								  PtrSize outputBufferSize, void* output)
+{
+	using UVecType = typename std::conditional_t<TCHANNEL_COUNT == 3, U8Vec3, U8Vec4>;
+	using FVecType = typename std::conditional_t<TCHANNEL_COUNT == 3, Vec3, Vec4>;
+
+	const UVecType* inPixels = static_cast<const UVecType*>(input);
+	UVecType* outPixels = static_cast<UVecType*>(output);
+
+	const U32 outWidth = inWidth >> 1;
+	const U32 outHeight = inHeight >> 1;
+
+	for(U32 h = 0; h < outHeight; ++h)
+	{
+		for(U32 w = 0; w < outWidth; ++w)
+		{
+			// Gather input
+			FVecType average(0.0f);
+			for(U32 y = 0; y < 2; ++y)
+			{
+				for(U32 x = 0; x < 2; ++x)
+				{
+					const U32 idx = (h * 2 + y) * inWidth + (w * 2 + x);
+					ANKI_ASSERT((idx + 1) * sizeof(UVecType) < inputBufferSize);
+					const UVecType inPixel = inPixels[idx];
+					for(U32 c = 0; c < TCHANNEL_COUNT; ++c)
+					{
+						average[c] += F32(inPixel[c]) / 255.0f * 0.25f;
+					}
+				}
+			}
+
+			UVecType uaverage;
+			for(U32 c = 0; c < TCHANNEL_COUNT; ++c)
+			{
+				uaverage[c] = U8(average[c] * 255.0f);
+			}
+
+			// Store
+			const U32 idx = h * outWidth + w;
+			ANKI_ASSERT((idx + 1) * sizeof(UVecType) < outputBufferSize);
+			outPixels[idx] = uaverage;
+		}
+	}
+}
+
+static ANKI_USE_RESULT Error compressS3tc(GenericMemoryPoolAllocator<U8> alloc, CString tempDirectory,
+										  const void* inPixels, U32 inWidth, U32 inHeight, U32 componentCount,
+										  void* outPixels, PtrSize outPixelsBufferSize)
+{
+	ANKI_ASSERT(inPixels);
+	ANKI_ASSERT(inWidth > 0 && isPowerOfTwo(inWidth) && inHeight > 0 && isPowerOfTwo(inHeight));
+	ANKI_ASSERT(outPixels);
+	ANKI_ASSERT(outPixelsBufferSize > 0);
+
+	class CleanupFile
+	{
+	public:
+		StringAuto m_fileToDelete;
+
+		CleanupFile(GenericMemoryPoolAllocator<U8> alloc, CString filename)
+			: m_fileToDelete(alloc, filename)
+		{
+		}
+
+		~CleanupFile()
+		{
+			if(!m_fileToDelete.isEmpty())
+			{
+				const int ret = std::remove(m_fileToDelete.cstr());
+				if(ret == 0)
+				{
+					ANKI_IMPORTER_LOGE("Couldn't delete file: %s", m_fileToDelete.cstr());
+				}
+			}
+		}
+	};
+
+	// Create a BMP image to feed to the compressor
+	StringAuto bmpFilename(alloc);
+	bmpFilename.sprintf("%s/%u_.bmp", tempDirectory.cstr(), U32(std::rand()));
+	if(!stbi_write_bmp(bmpFilename.cstr(), inWidth, inHeight, componentCount, inPixels))
+	{
+		ANKI_IMPORTER_LOGE("STB failed to create: %s", bmpFilename.cstr());
+		return Error::FUNCTION_FAILED;
+	}
+	CleanupFile bmpCleanup(alloc, bmpFilename);
+
+	// TODO
+
+	return Error::NONE;
+}
+
+static ANKI_USE_RESULT Error importImageInternal(const ImageImporterConfig& config)
+{
+	// Checks
+	ANKI_CHECK(checkConfig(config));
+	U32 width, height, channelCount;
+	ANKI_CHECK(checkInputImages(config, width, height, channelCount));
+
+	// Init image
+	GenericMemoryPoolAllocator<U8> alloc = config.m_allocator;
+	ImageImporterContext ctx(alloc);
+
+	ctx.m_width = width;
+	ctx.m_height = height;
+	ctx.m_depth = (config.m_type == ImageBinaryType::_3D) ? config.m_inputFilenames.getSize() : 1;
+	ctx.m_faceCount = (config.m_type == ImageBinaryType::CUBE) ? 6 : 1;
+	ctx.m_layerCount = (config.m_type == ImageBinaryType::_2D_ARRAY) ? config.m_inputFilenames.getSize() : 1;
+
+	U32 desiredChannelCount;
+	if(config.m_noAlpha || channelCount == 1)
+	{
+		// no alpha or 1 component grey
+		desiredChannelCount = 3;
+	}
+	else if(channelCount == 2)
+	{
+		// grey with alpha
+		desiredChannelCount = 4;
+	}
+	else
+	{
+		desiredChannelCount = channelCount;
+	}
+
+	ctx.m_channelCount = desiredChannelCount;
+	ctx.m_pixelSize = ctx.m_channelCount;
+
+	// Load first mip from the files
+	ANKI_CHECK(loadFirstMipmap(config, ctx));
+
+	// Generate mipmaps
+	const U32 mipCount = (config.m_type == ImageBinaryType::_3D)
+							 ? computeMaxMipmapCount3d(width, height, ctx.m_depth, config.m_minMipmapDimension)
+							 : computeMaxMipmapCount2d(width, height, config.m_minMipmapDimension);
+	for(U32 mip = 1; mip < mipCount; ++mip)
+	{
+		ctx.m_mipmaps.emplaceBack(alloc);
+
+		if(config.m_type != ImageBinaryType::_3D)
+		{
+			ctx.m_mipmaps[mip].m_surfacesOrVolume.create(ctx.m_faceCount * ctx.m_layerCount, alloc);
+			for(U32 l = 0; l < ctx.m_layerCount; ++l)
+			{
+				for(U32 f = 0; f < ctx.m_faceCount; ++f)
+				{
+					const U32 idx = l * ctx.m_faceCount + f;
+					const SurfaceOrVolumeData& inSurface = ctx.m_mipmaps[mip - 1].m_surfacesOrVolume[idx];
+					SurfaceOrVolumeData& outSurface = ctx.m_mipmaps[mip].m_surfacesOrVolume[idx];
+					outSurface.m_pixels.create((ctx.m_width >> mip) * (ctx.m_height >> mip) * ctx.m_pixelSize);
+
+					if(ctx.m_channelCount == 3)
+					{
+						generateSurfaceMipmap<3>(inSurface.m_pixels.getBegin(), inSurface.m_pixels.getSizeInBytes(),
+												 ctx.m_width >> (mip - 1), ctx.m_height >> (mip - 1),
+												 outSurface.m_pixels.getSizeInBytes(), outSurface.m_pixels.getBegin());
+					}
+					else
+					{
+						ANKI_ASSERT(ctx.m_channelCount == 4);
+						generateSurfaceMipmap<4>(inSurface.m_pixels.getBegin(), inSurface.m_pixels.getSizeInBytes(),
+												 ctx.m_width >> (mip - 1), ctx.m_height >> (mip - 1),
+												 outSurface.m_pixels.getSizeInBytes(), outSurface.m_pixels.getBegin());
+					}
+				}
+			}
+		}
+		else
+		{
+			ANKI_ASSERT(!"TODO");
+		}
+	}
+
+	// Compress
+	if(!!(config.m_compressions & ImageBinaryDataCompression::S3TC))
+	{
+		for(U32 mip = 0; mip < mipCount; ++mip)
+		{
+			for(U32 l = 0; l < ctx.m_layerCount; ++l)
+			{
+				for(U32 f = 0; f < ctx.m_faceCount; ++f)
+				{
+					const U32 idx = l * ctx.m_faceCount + f;
+					SurfaceOrVolumeData& surface = ctx.m_mipmaps[mip].m_surfacesOrVolume[idx];
+					// ANKI_CHECK(compressS3tc(config, ctx, surface.m_pixels.getBegin(), ctx.m_width >> mip,
+					//						ctx.m_height >> mip));
+				}
+			}
+		}
+	}
+
+	return Error::NONE;
+}
+
 Error importImage(const ImageImporterConfig& config)
 {
 	const Error err = importImageInternal(config);

+ 3 - 0
AnKi/Importer/ImageImporter.h

@@ -14,6 +14,8 @@ namespace anki
 /// @addtogroup importer
 /// @{
 
+/// Config for importImage().
+/// @relates importImage.
 class ImageImporterConfig
 {
 public:
@@ -25,6 +27,7 @@ public:
 	U32 m_minMipmapDimension = 4;
 	U32 m_mipmapCount = MAX_U32;
 	Bool m_noAlpha = true;
+	CString m_tempDirectory;
 };
 
 /// Converts images to AnKi's specific format.

+ 1 - 13
AnKi/Resource/ImageLoader.cpp

@@ -4,22 +4,10 @@
 // http://www.anki3d.org/LICENSE
 
 #include <AnKi/Resource/ImageLoader.h>
+#include <AnKi/Resource/Stb.h>
 #include <AnKi/Util/Logger.h>
 #include <AnKi/Util/Filesystem.h>
 
-#define STB_IMAGE_IMPLEMENTATION
-#define STBI_ASSERT(x) ANKI_ASSERT(x)
-#if ANKI_COMPILER_GCC_COMPATIBLE
-#	pragma GCC diagnostic push
-#	pragma GCC diagnostic ignored "-Wfloat-conversion"
-#	pragma GCC diagnostic ignored "-Wconversion"
-#	pragma GCC diagnostic ignored "-Wtype-limits"
-#endif
-#include <Stb/stb_image.h>
-#if ANKI_COMPILER_GCC_COMPATIBLE
-#	pragma GCC diagnostic pop
-#endif
-
 namespace anki
 {
 

+ 24 - 0
AnKi/Resource/Stb.cpp

@@ -0,0 +1,24 @@
+// Copyright (C) 2009-2021, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#include <AnKi/Resource/Common.h>
+
+#define STB_IMAGE_IMPLEMENTATION
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+
+#if ANKI_COMPILER_GCC_COMPATIBLE
+#	pragma GCC diagnostic push
+#	pragma GCC diagnostic ignored "-Wfloat-conversion"
+#	pragma GCC diagnostic ignored "-Wconversion"
+#	pragma GCC diagnostic ignored "-Wtype-limits"
+#	pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#	pragma GCC diagnostic ignored "-Wsign-compare"
+#endif
+
+#include <AnKi/Resource/Stb.h>
+
+#if ANKI_COMPILER_GCC_COMPATIBLE
+#	pragma GCC diagnostic pop
+#endif

+ 11 - 0
AnKi/Resource/Stb.h

@@ -0,0 +1,11 @@
+// Copyright (C) 2009-2021, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#include <AnKi/Resource/Common.h>
+
+#define STBI_ASSERT(x) ANKI_ASSERT(x)
+#define STBI_NO_FAILURE_STRINGS 1 // No need
+#include <Stb/stb_image.h>
+#include <Stb/stb_image_write.h>

+ 8 - 8
AnKi/Util/StringList.h

@@ -55,10 +55,10 @@ public:
 
 	/// Push at the end of the list a formated string.
 	template<typename... TArgs>
-	void pushBackSprintf(Allocator alloc, const TArgs&... args)
+	void pushBackSprintf(Allocator alloc, CString fmt, TArgs... args)
 	{
 		String str;
-		str.sprintf(alloc, args...);
+		str.sprintf(alloc, fmt, args...);
 
 		Base::emplaceBack(alloc);
 		Base::getBack() = std::move(str);
@@ -66,10 +66,10 @@ public:
 
 	/// Push at the beginning of the list a formated string.
 	template<typename... TArgs>
-	void pushFrontSprintf(Allocator alloc, const TArgs&... args)
+	void pushFrontSprintf(Allocator alloc, CString fmt, TArgs... args)
 	{
 		String str;
-		str.sprintf(alloc, args...);
+		str.sprintf(alloc, fmt, args...);
 
 		Base::emplaceFront(alloc);
 		Base::getFront() = std::move(str);
@@ -140,16 +140,16 @@ public:
 
 	/// Push at the end of the list a formated string
 	template<typename... TArgs>
-	void pushBackSprintf(const TArgs&... args)
+	void pushBackSprintf(CString fmt, TArgs... args)
 	{
-		Base::pushBackSprintf(m_alloc, args...);
+		Base::pushBackSprintf(m_alloc, fmt, args...);
 	}
 
 	/// Push at the beginning of the list a formated string
 	template<typename... TArgs>
-	void pushFrontSprintf(const TArgs&... args)
+	void pushFrontSprintf(CString fmt, TArgs... args)
 	{
-		Base::pushFrontSprintf(m_alloc, args...);
+		Base::pushFrontSprintf(m_alloc, fmt, args...);
 	}
 
 	/// Push back plain CString.

+ 3 - 2
Tools/Texture/TextureViewerMain.cpp

@@ -108,7 +108,8 @@ private:
 			StringListAuto mipLabels(getFrameAllocator());
 			for(U32 mip = 0; mip < grTex.getMipmapCount(); ++mip)
 			{
-				mipLabels.pushBackSprintf("Mip %u (%llux%llu)", mip, grTex.getWidth() >> mip, grTex.getHeight() >> mip);
+				mipLabels.pushBackSprintf("Mip %u (%llu x %llu)", mip, grTex.getWidth() >> mip,
+										  grTex.getHeight() >> mip);
 			}
 
 			const U32 lastCrntMip = m_crntMip;
@@ -235,7 +236,7 @@ public:
 
 		// Change window name
 		StringAuto title(alloc);
-		title.sprintf("%s %llux%llu Mips %u", argv[1], tex->getWidth(), tex->getHeight(),
+		title.sprintf("%s %llu x %llu Mips %u", argv[1], tex->getWidth(), tex->getHeight(),
 					  tex->getGrTexture()->getMipmapCount());
 		getWindow().setWindowTitle(title);