Repository: godotengine/godot (language: cpp)

Issue:
RGBA DDS files with a non-power of two resolution  and mipmaps fail to be read
### Tested versions

Reproducible in 4.5-dev1

### System information

Godot v4.5.dev1 - Windows 10 (build 19045) - Multi-window, 1 monitor - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 2060 (NVIDIA; 32.0.15.6094) - Intel(R) Core(TM) i9-9900KF CPU @ 3.60GHz (16 threads)

### Issue description

Attempting to load a dds file with the RGBA8 format, mipmaps, and a non-power of 2 size will result in a failure between the expected and actual file size, and so the file will fail to load. 

### Steps to reproduce

Open the attached project and read the error message printed when dds_120_with_mipmaps.dds is clicked on.

### Minimal reproduction project (MRP)

[dds_mipmap_test.zip](https://github.com/user-attachments/files/19642255/dds_mipmap_test.zip)


Discussion / hints:
Correct me if I'm wring, but dds must be power of two, this has been a rule since forever.
I can confirm this on 4.5.dev af2c71397 (Linux). This issue only affects DDS files that contain mipmaps.

**Testing project:** [test_dds.zip](https://github.com/user-attachments/files/19651942/test_dds.zip)

Here, the 512×384 DDS with mipmaps fails to load, but all other DDS files render correctly:

![Image](https://github.com/user-attachments/assets/84b71b31-557a-4d70-8e75-aa6145f66a46)

```
  ERROR: Expected Image data size of 512x384x1 (DXT5 RGBA8 with 9 mipmaps) = 262192 bytes, got 262176 bytes instead.
  ERROR: Invalid image: image is empty
  ERROR: Failed loading resource: res://512x384_dxt5_mipmaps.dds. Make sure resources have been imported by opening the project in the editor at least once.
```

The image renders correctly in Dolphin's preview, so it is a valid DDS file:

![Image](https://github.com/user-attachments/assets/d96d4afb-bcca-4473-9088-7df97338f9f8)
Then it sounds like:
* https://github.com/godotengine/godot/issues/96635
> Correct me if I'm wring, but dds must be power of two, this has been a rule since forever.

But why is that the rule? These are valid DDS files that many other software applications can use. I understand that it may not have been an issue before, as it is best practice for textures to be a power of 2. But I'm using the new load_dds_from_buffer method to load DDS files from an external file that I do not have control of, so I can't manually resize them. For a bit more context I'm making a tool to help mod another game and am trying to get it to automatically load in the game assets and the mod's assets. It works for everything except for the DDS files.
CC @godotengine/import 
The issue is due to the dds loader calculating the full image data size in a slightly different way than the engine itself, so at certain edge cases that divergence causes an error
This also could be because Godot has 2 different ways to calculate mipmap sizes in different cases and only one of them is correct.

1. https://github.com/godotengine/godot/blob/master/modules/dds/texture_loader_dds.cpp#L152
2. https://github.com/godotengine/godot/blob/master/modules/dds/texture_loader_dds.cpp#L168

i remember fixing something like this in the past, I can check and provide some patches if nobody has concerns against that.
I downloaded the texture that @thundershield provided and extracted all mipmaps out of it using nvidia tool:
```
nvdecompress -mipmaps -format png ../dds_120_with_mipmaps.dds
```

And got ethalon mipmap resolutions:
```
>file *.png
dds_120_with_mipmaps_mipmap0.png: PNG image data, 120 x 120, 8-bit/color RGBA, non-interlaced
dds_120_with_mipmaps_mipmap1.png: PNG image data, 60 x 60, 8-bit/color RGBA, non-interlaced
dds_120_with_mipmaps_mipmap2.png: PNG image data, 30 x 30, 8-bit/color RGBA, non-interlaced
dds_120_with_mipmaps_mipmap3.png: PNG image data, 15 x 15, 8-bit/color RGBA, non-interlaced
dds_120_with_mipmaps_mipmap4.png: PNG image data, 7 x 7, 8-bit/color RGBA, non-interlaced
dds_120_with_mipmaps_mipmap5.png: PNG image data, 3 x 3, 8-bit/color RGBA, non-interlaced
dds_120_with_mipmaps_mipmap6.png: PNG image data, 1 x 1, 8-bit/color RGBA, non-interlaced
```

I also extracted some code from godot library to be able to run it standalone. As you can see below, two different branches of code in Godot produce different mipmap sizes:
```
void mipmap_uncompressed() {
    std::cout<<"Mipmap calculator (uncompressed)\n";
    
    int p_mipmaps = 7;
    
    int w = 120;
    int h = 120;

    printf("mipmap=%d width=%d height=%d\n", 0, w, h);
	for (int i = 1; i < p_mipmaps; i++) {
		w = (w + 1) >> 1;
		h = (h + 1) >> 1;
		
		printf("mipmap=%d width=%d height=%d\n", i, w, h);
	}
}


Mipmap calculator (uncompressed)
mipmap=0 width=120 height=120
mipmap=1 width=60 height=60
mipmap=2 width=30 height=30
mipmap=3 width=15 height=15
mipmap=4 width=8 height=8
mipmap=5 width=4 height=4
mipmap=6 width=2 height=2

void mipmap_compressed() {
    std::cout<<"Mipmap calculator (compressed)\n";
    
    int p_mipmaps = 7;
    
    int w = 120;
    int h = 120;

    printf("mipmap=%d width=%d height=%d\n", 0, w, h);
	for (int i = 1; i < p_mipmaps; i++) {
		w = MAX(1u, w >> 1);
		h = MAX(1u, h >> 1);
		
		printf("mipmap=%d width=%d height=%d\n", i, w, h);
	}
}


Mipmap calculator (compressed)
mipmap=0 width=120 height=120
mipmap=1 width=60 height=60
mipmap=2 width=30 height=30
mipmap=3 width=15 height=15
mipmap=4 width=7 height=7
mipmap=5 width=3 height=3
mipmap=6 width=1 height=1
```

And if you [look into the DDSTextureLoader code by Microsoft](https://github.com/microsoft/DirectXTex/blob/main/DDSTextureLoader/DDSTextureLoader9.cpp#L894) (MIT license btw), it indeed uses MAX(1u, w >> 1) approach, so the `uncomressed` branch in Godot is incorrect.


I opened pull request for this: https://github.com/godotengine/godot/pull/105193

![Image](https://github.com/user-attachments/assets/7e3fb7f3-507c-46f3-921e-075b05703a22)
![Image](https://github.com/user-attachments/assets/515e10c3-d945-4733-99c7-dcfd6c3d9b3e)



UPD: I just realized that the change did not fix "512x384_dxt5_mipmaps.dds" provided by Calinou, it's compressed texture and fails in different if-else branch, checking for it.
Apologies for the multiple posts - I wanted to keep this explanation separate to avoid mixing it with the problem described in previous message.

The texture 512x384_dxt5_mipmaps, provided by Calinou [here](https://github.com/godotengine/godot/issues/105136#issuecomment-2786799904), fails to load for a different reason.

Although it's a compressed texture, meaning the incorrect mipmap width/height logic mentioned earlier doesn't directly affect it, there is still a slightly incorrect size calculation per level in Godot’s DDS loader, specifically [here](https://github.com/godotengine/godot/blob/master/modules/dds/texture_loader_dds.cpp#L143).


The formula that Godot uses is
```
MAX(info.divisor, w) / info.divisor * MAX(info.divisor, h) / info.divisor * info.block_size;
```

While [dds format docs recommend](
https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-file-layout-for-textures) following:
```
max(1, ( (width + 3) / 4 ) ) x max(1, ( (height + 3) / 4 ) ) x 8(DXT1) or 16(DXT2-5)
```

They seem produce same results for 2^n sized textures, but different when width and height is not divisible by 8 (or 16).

I printed out some values using two different formulas and indeed there is a difference:
```
>cat test.txt | grep wrong
  WARNING: modules\dds\texture_loader_dds.cpp:145 -  wrong mipmap 0: 512 x 384 = 196608
  WARNING: modules\dds\texture_loader_dds.cpp:163 -  wrong mipmap 1: 256 x 192 = 49152
  WARNING: modules\dds\texture_loader_dds.cpp:163 -  wrong mipmap 2: 128 x 96 = 12288
  WARNING: modules\dds\texture_loader_dds.cpp:163 -  wrong mipmap 3: 64 x 48 = 3072
  WARNING: modules\dds\texture_loader_dds.cpp:163 -  wrong mipmap 4: 32 x 24 = 768
  WARNING: modules\dds\texture_loader_dds.cpp:163 -  wrong mipmap 5: 16 x 12 = 192
  WARNING: modules\dds\texture_loader_dds.cpp:163 -  wrong mipmap 6: 8 x 6 = 48               <----------- wrong
  WARNING: modules\dds\texture_loader_dds.cpp:163 -  wrong mipmap 7: 4 x 3 = 16
  WARNING: modules\dds\texture_loader_dds.cpp:163 -  wrong mipmap 8: 2 x 1 = 16
  WARNING: modules\dds\texture_loader_dds.cpp:163 -  wrong mipmap 9: 1 x 1 = 16

>cat test.txt | grep correct
  WARNING: modules\dds\texture_loader_dds.cpp:148 -  correct mipmap 0: 512 x 384 = 196608
  WARNING: modules\dds\texture_loader_dds.cpp:166 -  correct mipmap 1: 256 x 192 = 49152
  WARNING: modules\dds\texture_loader_dds.cpp:166 -  correct mipmap 2: 128 x 96 = 12288
  WARNING: modules\dds\texture_loader_dds.cpp:166 -  correct mipmap 3: 64 x 48 = 3072
  WARNING: modules\dds\texture_loader_dds.cpp:166 -  correct mipmap 4: 32 x 24 = 768
  WARNING: modules\dds\texture_loader_dds.cpp:166 -  correct mipmap 5: 16 x 12 = 192
  WARNING: modules\dds\texture_loader_dds.cpp:166 -  correct mipmap 6: 8 x 6 = 64               <----------- correct
  WARNING: modules\dds\texture_loader_dds.cpp:166 -  correct mipmap 7: 4 x 3 = 16
  WARNING: modules\dds\texture_loader_dds.cpp:166 -  correct mipmap 8: 2 x 1 = 16
  WARNING: modules\dds\texture_loader_dds.cpp:166 -  correct mipmap 9: 1 x 1 = 16
```

Now, in Godot the formula should look like this:
```
MAX(1, (w + 3) / 4) * MAX(1, (h + 3) / 4) * info.block_size;
```

The only thing left is that the loader still prints warnings for DDS files with dimensions not divisible by 4, like this:
```
  WARNING: modules\dds\texture_loader_dds.cpp:137 - res://dds_mipmap_test/randomsize_bc7.dds: DDS width '165' is not divisible by 4. This is not allowed as per the DDS specification, attempting to load anyway.
  WARNING: modules\dds\texture_loader_dds.cpp:140 - res://dds_mipmap_test/randomsize_bc7.dds: DDS height '37' is not divisible by 4. This is not allowed as per the DDS specification, attempting to load anyway.
```



Archive with test dds'ses in different formats:
[dds_mipmap_compressed_test.zip](https://github.com/user-attachments/files/19668901/dds_mipmap_compressed_test.zip)

Screenshot:
![Image](https://github.com/user-attachments/assets/9bb65bd8-06c3-4898-8875-7e6f66666a5e)

PR: https://github.com/godotengine/godot/pull/105200


Fix patch:
diff --git a/modules/dds/texture_loader_dds.cpp b/modules/dds/texture_loader_dds.cpp
index 1fd7e5170557..c360dd8892ba 100644
--- a/modules/dds/texture_loader_dds.cpp
+++ b/modules/dds/texture_loader_dds.cpp
@@ -165,8 +165,8 @@ static Ref<Image> _dds_load_layer(Ref<FileAccess> p_file, DDSFormat p_dds_format
 		uint32_t size = p_width * p_height * info.block_size;
 
 		for (uint32_t i = 1; i < p_mipmaps; i++) {
-			w = (w + 1) >> 1;
-			h = (h + 1) >> 1;
+			w = MAX(1u, w >> 1);
+			h = MAX(1u, h >> 1);
 			size += w * h * info.block_size;
 		}
 


Test patch: