lowmiptextures - themeldingwars/Documentation GitHub Wiki
lowmiptextures.pak lives in system/vt next to the virtual texture files. It is the
low mip fallback pack: the smallest mip levels of every texture in the virtual texture,
bundled into one archive so the renderer always has something to draw while the real tiles are
still being streamed in. The client side of this is lowmiptexturecache.cpp, and
fx.lowMipTextureThreshold is the cvar that decides when it gets used.
Unlike vtex it is a completely ordinary archive: an LZMA compressed index followed by one
LZMA compressed block per texture. Everything needed to unpack it is in the file itself, no
.vtex_idx involved.
The index is essentially the DDS header of each full size texture, so turning an entry back
into a .dds with only the stored mips is a matter of writing that header with the size
fields shifted down by firstMip and appending the data. The 1962 pak has 2'464 entries,
FauFau decodes all of them.
010 Editor Templates
- lowmiptextures.bt - the container
- lowmiptextures.index.bt - run this on the decompressed index
Implementations
- Forgery unpacks it, see the
vtex test-mipscommand in forgery.js - FauFau reads it and writes the entries out as DDS files
- Client: the loader is at
0x012b44b0in build 1962.
Format
Little endian.
Container
| Field | Type | Notes |
|---|---|---|
magic |
char[4] |
LMTD |
indexOriginalSize |
uint32 |
Decompressed size of the index |
indexCompressedSize |
uint32 |
Compressed size of the index, including the LZMA properties |
index |
byte[indexCompressedSize] |
LZMA |
blocks |
byte[] |
The texture data, to the end of the file |
blocks starts at offset 12 + indexCompressedSize. Call that the base offset, every
compressedOffset in the index is relative to it.
Every LZMA stream in the file, the index and each block, is 5 property bytes followed by the
raw stream. There is no 8 byte size like in an .lzma file, the decompressed size always comes
from the surrounding fields.
Index
Once decompressed:
| Field | Type |
|---|---|
rowCount |
uint32 |
rows |
Row[rowCount] |
Row
The fields after the path are the fields of a DDS_HEADER for the full size texture, in the
same order, without the size field and the reserved ones. The client copies them straight into
a DDS header.
| Field | Type | Notes |
|---|---|---|
path |
cstring |
Asset path of the texture, like 00118000\00118090.dds |
flags |
uint32 |
dwFlags |
width |
uint32 |
Of the full size texture |
height |
uint32 |
Of the full size texture |
depth |
uint32 |
|
pitchOrLinearSize |
uint32 |
Of the full size texture |
mipMapCount |
uint32 |
Of the full size texture |
pixelFormat |
byte[32] |
DDS_PIXELFORMAT, the FourCC like DXT5 is at offset 8 |
caps |
byte[16] |
dwCaps to dwCaps4 |
firstMip |
uint32 |
Index of the largest stored mip |
decompressedSize |
uint32 |
Size of this texture's data once decompressed |
compressedOffset |
uint32 |
Offset of the block, relative to the base offset |
compressedSize |
uint32 |
Size of the block, including the LZMA properties |
Rows are variable length because of the leading path string, so the index has to be walked from the start.
Blocks
Each block is a standalone LZMA stream with its own properties: slice compressedSize bytes at
baseOffset + compressedOffset and decompress to exactly decompressedSize bytes. The rows
appear in offset order in practice, but nothing depends on that.
The data holds the mips from firstMip down to the smallest one, back to back, the same way
they'd follow the header in a .dds. To build the DDS header for it, shift width and
height right by firstMip (at least 1), take mipMapCount - firstMip as the mip count and
shift pitchOrLinearSize by firstMip if flags has the pitch bit (0x8) set, by
2 * firstMip otherwise.