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.
Note |
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!
–Neil