Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TPC (Texture Pack Compressed)

TPC is the proprietary bundled texture format created by BioWare. It contains the raw DXT-compressed texture data, pre-computed mipmaps, and potentially appended TXI configuration data all in one blob.

At a Glance

PropertyValue
Extension(s).tpc
Magic SignatureNone
TypeCompressed Texture Pack
Rust ReferenceView rakata_formats::Tpc in Rustdocs

File Layout

Population for everything on this page: every .tpc in the four ERF archives under TexturePacks/ in a retail PC install. Three of them, swpc_tex_tpa, tpb and tpc, hold the same resrefs at three resolutions; the fourth, swpc_tex_gui, holds the interface textures. None of the four is indexed by chitin.key, which is worth knowing before writing anything that enumerates game resources by walking the KEY file: do that and you will conclude, wrongly, that the game ships no textures in this format.

A fixed 128-byte header, the texture payload immediately after it, and optionally some TXI text tacked onto the end. Nothing points anywhere; the payload always starts at 0x80.

BlockSizeLocated by
Header128 bytesAlways at 0x00
Texture payloaddata_size, plus the mip chainAlways at 0x80
TXI footerremainder of the fileWhatever follows the payload

Header (128 bytes)

OffsetFieldTypeNotes
0x00data_sizeu32Not one quantity. 0 means uncompressed; otherwise see below.
0x04alpha_testf32Read and handed downstream. No consumer found; see below.
0x08widthu16
0x0Aheightu16
0x0Cpixel_typeu8Three values occur on disk: 1, 2 and 4. See below.
0x0Dmipmap_countu8Load-bearing. The engine iterates it directly and does not check it. See below.
0x0Ereserved114 bytes

There is no magic signature. A .tpc is identified by its extension and by the header parsing plausibly, which is worth knowing before writing anything that sniffs file types.

Warning

mipmap_count is the one header field a writer cannot get away with approximating It is not descriptive. The loader uses it as the iteration count of the loop that accumulates the mip chain’s size, on both the compressed and the uncompressed path, and there is nothing else in that function deriving a level count from width and height. The stored byte is simply trusted, with no bounds check against how many levels the dimensions could actually support.

So this value is how the engine learns where the pixel data ends. Everything after the pixel data, meaning the TXI tail this page describes further down, is found by starting where the mip chain stopped. A writer must emit the exact number of levels it actually wrote, all the way down to 1x1. Write a count that is too small and the trailer is read from inside the pixel data; too large and the loop walks off the end of it. Either way every offset past the payload is wrong, and the failure is silent rather than a rejection.

The unclamped arithmetic in the next box is what turns that count into byte offsets, so the two rules are one rule in practice: emit every level, count every level.

(Provenance: traced, at CResTPC::OnResourceServiced (0x00712ff0).)

Warning

Mip sizes are computed, not read, and the arithmetic does not clamp The engine does not use the stored dimensions per level. It halves the base width and height for each level with a plain right shift and no clamp to a minimum of 1, so once a dimension reaches zero the level contributes no bytes at all. A deep enough mip chain therefore has trailing levels that occupy nothing. Rakata reproduces this exactly rather than the more usual clamp-at-one behaviour, because a writer that clamps produces a payload the engine will read at the wrong offsets from that level onward.

pixel_type, on disk and internally

The engine checks this byte bit by bit to produce an internal format code, and the audit below describes that internal value. An implementer reads the byte, which is one step removed. Three values occur:

On diskFormatPayload size per level
2DXT18 bytes per 4x4 block
4DXT516 bytes per 4x4 block
1uncompressed, one byte per pixelwidth x height

2 and 4 are the bulk of the corpus and split it roughly evenly. 1 is rare, a dozen textures, and it is the value a reader written against the two-value description meets and cannot handle. Its payload reproduces the file length at one byte per pixel, and the audit below already records the engine deriving an internal code 1 from this bit that the DXT dispatch then does not consume.

No DXT3 value appears, matching the absence of any DXT3 path in the parser, so a DXT3 payload is not a variant handled badly. It is one not handled.

Important

One byte per pixel is luminance, not an alpha mask and not a palette Those two are what a reader guesses when it meets a single-byte format, and both are wrong. Following the uncompressed path to the call that actually hands the texture to OpenGL, the internal code this byte produces selects GL_LUMINANCE with unsigned-byte components. The single channel is replicated across red, green and blue and the texture draws grey, rather than modulating another image’s transparency or indexing into a colour table.

That is read off the instruction choosing the GL internal format, not inferred from the byte count, which is why it can contradict the guess rather than merely differ from it.

A caution about which code space you are in. The disk values are 1, 2 and 4, and the codes that GL dispatch branches on are its own. They overlap numerically without meaning the same thing: disk 4 is DXT5 and goes down the compressed path, never reaching this branch. The same GL function also handles codes for GL_RGB, GL_RGBA and a packed 16-bit format, reached from callers other than the TPC uncompressed path, so its branch list is not a list of TPC pixel types. Read the disk byte, and treat the mapping above as covering the one disk value that arrives here.

Note

alpha_test is populated and goes nowhere this pass could follow It is a genuine f32 and the loader does read it. Both internal consumers reachable from there take it into a local and never mention it again. It also escapes through a public accessor that hands every header attribute back to its caller unexamined, and that caller is reached only through a vtable slot with no static call site in the executable.

So this is a bounded negative rather than a dead field: no consumer was found within the depth traced, and the trail ends at an indirection rather than at a conclusion. The distinction matters for a writer, because “nothing reads it” and “you may put anything there” are different claims and only a genuinely dead field licenses the second. Preserve the value on a round trip.

data_size

Warning

Uncompressed is not an edge case here data_size is 0 in roughly one texture in seven, and those are not one pixel type: the zero appears against 1, 2 and 4 alike. A reader that treats an uncompressed TPC as a curiosity, or that infers the encoding from pixel_type alone without checking data_size first, mis-sizes a substantial slice of the shipped textures.

Warning

data_size carries at least three meanings Where it is non-zero it usually is the base level’s byte count, and for most textures reading it that way is correct. Two groups depart from it, and one of them departs by a factor of six.

Cubemaps store six faces and data_size sizes one. Every texture in this group is named CM_* and has a height six times its width: six square faces stacked vertically in one image. The base level computed from width x height is exactly six times the stored value. A reader trusting data_size to bound the base level reads one face and calls it the texture; a reader trusting the dimensions reads six times what the field says.

A second group stores more than the base level, close to the whole mip chain rather than the first level of it. C_HoloDodonna at 512x512 DXT1 has a 131,072-byte base and a data_size of 174,816. What rule produces those figures is unexplained: the group is identifiable by the field exceeding the computed base, and beyond that this page does not know.

The way out is that nothing needs this field. The mip arithmetic below derives every level from width, height and pixel_type, and it reproduces the payload without consulting data_size at all. Treat the field as something to preserve on round trip rather than something to read.

The mip chain

Note

The mip formula is confirmed against shipped textures, by tiling Summing the chain with the unclamped shift and consulting nothing else, the computed payload overruns none of the textures in the TexturePacks/ archives. It lands exactly on the end of the file in a little under half of them, and in the rest the remainder past it is the TXI tail this page describes, opening on mipmap, envmaptexture, clamp or another directive. There is no file the arithmetic overshoots.

That is the same tiling check that settles GFF and LIP.

It discriminates where it matters: on every file whose chain reaches a zero dimension, the no-clamp model reproduces the file’s length and a clamp-at-one model does not. Where the two give different totals, file length refutes the clamping reader.

The 114-byte reserved region is zero in every file tested, at every byte position.

Engine Audits & Decompilation

Read from CAuroraProcessedTexture::ReadProcessedTextureHeader at 0x0070f590 in swkotor.exe. Provenance: derived, not attested for the rows below, which have not been separately re-derived and sit on the reverse-engineering queue. The file layout above is no longer in that class: it is measured, against the archives under TexturePacks/.

Pipeline EventEngine Behaviour
Format Byte MappingThe header’s format byte is read as a bitmask. The engine tests bit 0, bit 1 and bit 2 to produce internal format codes 1, 3 and 4.
Compression DispatchOnly two of those reach the compressed path: code 3 reads 8-byte blocks (DXT1) and code 4 reads 16-byte blocks (DXT5). Nothing else dispatches.
Mipmap CalculationsLevel dimensions are computed by right-shifting the base dimensions, with no clamp to a minimum of 1. A deep enough chain reaches zero and those levels contribute no bytes.
OpenGL Hardware BindingCode 3 maps to 0x83F0 (DXT1) and code 4 to 0x83F3 (DXT5). There is no branch for DXT3 (0x83F2) anywhere in the parser.