Unclear behavior of nvtt::countMipmaps


ever since we’ve updated to using the texture tools version 2023.2.0 (from the older version from 2021) I’ve noticed some odd behavior with the amount of mipmap levels.

Before it would behave similarly to other dds programs (like visual studio, unity etc) but now it always appears to create too many mipmaps, and when you give it a 4x4 image, it creates 3 mipmaps, each being 4x4, so the same size as a BC1 block in pixel.

I’ve tried to figure out the cause of this and noticed that giving weird pixel values to nvtt::countMipmaps returns different values than you would expect (see code block below, this was from a simple test application where I passed in the values and printed the output values to console)

2048x2048: 12
4096x2048: 13
2048x4096: 13
2000x2000: 11
1x1: 2

This is causing some weird issues in the use case we’re using the program for and I’m not sure what got changed which causes this change since we’ve used the program for around 2 years with 0 issues.

The too many mipmaps seems like an issue (unless it’s not an issue, but then I wouldn’t know the reason why this would be necessary)

And also a fature to limit the amount of mipmaps (or set the minimum resolution) would be great also.

Hi @niekschoemaker!

Thank you for the questions. I’ve been able to reproduce the same values from nvtt::countMipmaps(), except for the 1x1 case. Could you confirm if this is how you’re calling the function? (I’m showing the C wrapper function below; the C++ code is the same except using nvtt::countMipmaps(x, y, z) instead.)

  printf("2048x2048: %i\n", nvttCountMipmaps(2048, 2048, 1, NULL));
  printf("4096x2048: %i\n", nvttCountMipmaps(4096, 2048, 1, NULL));
  printf("2048x4096: %i\n", nvttCountMipmaps(2048, 4096, 1, NULL));
  printf("2000x2000: %i\n", nvttCountMipmaps(2000, 2000, 1, NULL));
  printf("1x1: %i\n", nvttCountMipmaps(1, 1, 1, NULL));

This produces the following output:

2048x2048: 12
4096x2048: 13
2048x4096: 13
2000x2000: 11
1x1: 1

I believe these values are correct. The key to this mystery is probably that even though BC1 blocks are 4x4 pixels, we can BC-compress images with widths and heights that aren’t powers of 2.

Here’s an image from Microsoft’s Direct3D 10 reference page that shows how this works.

Here, mip 0 is 60x40 (width x height), so mip 1 is 30x20. Although viewing the texture only shows 30x20 pixels, the data for this mip actually contains 8 columns and 5 rows of BC blocks – enough data for 32x20 pixels. Similarly, mip 2 is 15x10, and its compressed data 4 columns and 3 rows of BC blocks (enough data for 16x12 pixels).

(Two other places where this logic appears are in Microsoft’s DDS Texture Example and “in the levelImages loop above” in Section 2 of the KTX v2 specification.)

Modern APIs (since Direct3D 10) usually do a good job of abstracting this detail away; the DDS header for a BC1-compressed 15x15 image, for instance, has a width and height of 15 and acts like a raw RGBA 15x15 image, even if it has blocks that overlap the right and bottom borders of the image. GPUs know what logic to perform to look up texel values correctly in such cases.

Some old APIs reject images with mips where the width and height are not divisible by the block width and height. In that case, you’ll want to count mipmaps using nvtt::Surface::countMipmaps(4) when using BC compression, and verify that the width and height of your input images are powers of 2. Thankfully, newer APIs don’t have this restriction.

This can produce some surprising things if you look at it closely – for instance, 4x4, 2x2, and 1x1 mips all use the same number of blocks (1) and bytes under BC compression.

With that in mind, here are the 13 mip sizes nvtt::countMipmaps() computes for a 4096x2048 texture (and the layout of the BC blocks for each mip):

Mip 0:  4096x2048 (1024x512 blocks)
Mip 1:  2048x1024 (512x256 blocks)
Mip 2:  1024x512  (256x128 blocks)
Mip 3:  512x256   (128x64 blocks)
Mip 4:  256x128   (64x32 blocks)
Mip 5:  128x64    (32x16 blocks)
Mip 6:  64x32     (16x8 blocks)
Mip 7:  32x16     (8x4 blocks)
Mip 8:  16x8      (4x2 blocks)
Mip 9:  8x4       (2x1 blocks)
Mip 10: 4x2       (1x1 block)
Mip 11: 2x1       (1x1 block)
Mip 12: 1x1       (1x1 block)

(It might be surprising that there are both 2x1 and 1x1 mips in a full mip chain, but it’s true! See section 3.7, “levelCount”, in the KTX 2 specification.)

Thanks, and let me know if you have any questions!