MorsGames / sm64plus

A fork of sm64-port that focuses on QoL improvements and customizability.
https://mfgg.net/index.php?act=resdb&param=02&c=2&id=38190
464 stars 32 forks source link

Fix custom texture loading on Linux #104

Open eohannes opened 3 months ago

eohannes commented 3 months ago

This patch fixes the missing :tm: texture on the Linux intro screen, as well as every other missing texture that I encountered while trying to get this running on my Steam Deck.

SM64+ can load custom textures from PNG files on the disk. It knows where to find those files because in each leveldata.c there exist a number of texture declarations that look like this:

ALIGNED8 static const Texture intro_seg7_texture_0700B4A0[] = {
#include "levels/intro/2_copyright.rgba16.inc.c"
};

// ...

ALIGNED8 static const Texture intro_seg7_texture_0700C4A0[] = {
#include "levels/intro/3_tm.rgba16.inc.c"
};

Those #included files look like this:

0x6C,0x65,0x76,0x65,0x6C,0x73,0x2F,0x69,0x6E,0x74,0x72,0x6F,0x2F,0x33,0x5F,0x74,0x6D,0x2E,0x72,0x67,0x62,0x61,0x31,0x36,

This is a hex literal representation of levels/intro/3_tm.rgba16. When the game loads the :tm: texture, it looks up the symbol intro_seg7_texture_0700C4A0 in leveldata.o, which, per my hex editor, looks like this:

000001f0: 6c65 7665 6c73 2f69 6e74 726f 2f33 5f74  levels/intro/3_t
00000200: 6d2e 7267 6261 3136 6c65 7665 6c73 2f69  m.rgba16levels/i
00000210: 6e74 726f 2f32 5f63 6f70 7972 6967 6874  ntro/2_copyright
00000220: 2e72 6762 6131 3600 0000 0000 0000 0000  .rgba16.........

The game then tries to load a file named levels/intro/3_tm.rgba16levels/intro/2_copyright.rgba16.png. Naturally, because there is no file by the name, the lookup fails, and we are left with the vague impression that we've forgotten to install Counter-Strike: Source. But the ©️ texture loads just fine. What's going on here?

Let's inspect that last line. Why is .rgba16 followed by a bunch of zero bytes? Because these Textures are ALIGNED8, or __attribute__((aligned(8))); that is, we've told the compiler to pad them such that they always start on an offset divisible by eight. So when we execute the snprintf that resolves the path for custom textures, it reads to the first 00 byte and interprets it as a null terminator. But when we read the :tm: texture path, we're starting at the top line of the hex output, and snprintf will read all the way to that same 00 byte, including the ©️ path with it.

The question remains: why isn't there a null terminator there? Well, that's easy. We didn't put one there when we generated the file. 3_tm.rgba16.inc.c doesn't have 0x00 at the end. And because the path's length is a multiple of eight bytes, the compiler doesn't bother to pad the end of it. So these two strings run together, and we fail to load the texture.

Why doesn't this happen on Windows? I have no idea. I know nothing about the Windows build process. My guess is that the linking process is just fundamentally different and we get lucky.

Why doesn't this happen to every texture on Linux? Because this only occurs when a texture is preceded immediately by another texture with a path length divisible by eight. There aren't a lot of those.

Why does rebuilding the game in a different directory seem to fix this? I don't know, and I couldn't reproduce that behavior on my Steam Deck, or on WSL, or in the Docker container.

To fix this, I added a null terminator to the end of all the .inc.c files. Now they behave like normal C strings. This may fix #85.