PAA File Format
PAA texture file structure
Introduction
Of the many image file formats out there, such as jpeg, or gif, Bohemia Interactive choose to use a specially developed file format (paa) as the base texture file for all engine types.
The reason for this is the raw data within the file can be passed directly to Microsoft's Direct X as a DXT1 picture (eg) without further massaging.
While all engines except Elite also support JPG files, PAA files can result in much better performance.
PAC files
.pac files are (almost) synonomous with .paa. In the origins of OFP cwc. paa were meant to be two tone colors (black & white or grayscale) and paC = colour.
The distinction does not exist. All engines treat pac or paa equally.
Note however that Arma2's tgaviewer *cannot* read a palete index .paa extension. only .pac extensions. (the contents can be identical)
OFP DEMO files
The initial 'proof of concept' for ofp released in 1997, was a demonstration of an island with models and textures.
These pac/paa files can still be read by tgaviewer! (and pal2pace)
The *only* difference between these, and subsequent pax files is that the index pallet appears at beginning of file. In other words
- There is NO header type, Pallet index is assumed.
- There are NO taggs.
- Other than this, demo files are 100% identical to ofp's Pallet index including the use of run length compression.
Main Format
Overall structure of a Paa file is
struct overall { ushort TypeOfPaa; //OPTIONAL Tagg Taggs[...]; //OPTIONAL Palette Palette[...]; MipMap MipMaps[...]; ushort Always0; };
TypeOfPaa (optional)
With the sole exception of OFP index palettes, all pac/paa files begin with a 2 byte 'type' signature. OFP index palettes have no type, they begin with tagg structures immediately.
- 0xFF01 DXT1 : All Engines
- 0xFF02 DXT2 : Oxygen 2 Only
- 0xFF03 DXT3
- 0xFF04 DXT4 : Oxygen 2 Only
- 0xFF05 DXT5 : Arma1& 2 Only
- 0x4444 RGBA 4:4:4:4 : OFP and Arma2 only (not Arma1)
- 0x1555 RGBA 5:5:5:1 : Arma1& 2 Only
- 0x8888 RGBA 8:8:8:8 : Oxygen 2 Only
- 0x8080 GRAY w Alpha : All Engines
'Index Pallettes only occur in ofp Later versions of TGAViewer still 'understand' them IF and only IF they have .pac extensions. Under this circumstance they are translated as 8080 gray types (which are not saveable anyway!)
Note that information on DXT formats can be found at http://www.opengl.org/wiki/S3_Texture_Compression
Taggs
Tagg { char signature[4]; //always "GGAT" char name[4]; //name of the tagg in reversed order (for example: "CGVA") ulong dataLen; byte data[dataLen]; }
Taggs are read until no "GGAT" signature is encountered. Palette structure follows.
General
Structurally, all TAGG's are optional and can occur multiple times. In practice:
- At least one TAGG exists for ALL types, including index palettes.
- Any TAGG if it occurs, only occurs, once.
- TAGG order of presentation is
AVG //Always First MAX //Always 2nd etc... FLAG SWIZ or PROC (not seen together) OFFS //Always last
- ALL TAGG's begin with an AVG with the sole exception of *some* GREY .paa's
- MAX is almost follows an AVG
- FLAG, PROC, and SWIZ are entirely optional. They may or may not occur.
- PROC if present is only used in DXT1 types
- OFP Palette Indexes ONLY have an AVG
- OFFS is almost always present (if not, the engine builds it as required, when required, anyway)
AVGCTAGG
Average Colour
- This tag contains average color of texture, probably used in rendering the 8:8 Grey & alpha textures.
- Always present, and always first in list with the sole exception of *some* GRAY .paa's
{ char "GGATCGVA"; ulong len; // 4 bytes ulong RGBA;//FF443D39 }
MAXCTAGG
- Generally always present. Always follows immediately after an AVG
{ char "GGATCXAM"; ulong len; // 4 bytes ulong data; // FFFFFFFFF no other value seen so far }
Contains color of brightest pixel in texture?
FLAGTAGG
- Optional, but will always occur immediately after a MAXC when present
{ char "GGATGALF"; ulong len; // 4 bytes ulong range; // 0 to 2 }
Marks if texture contains transparency. Value 1 means basic transparency, 2 means alpha channel is not interpolated. This flag should be always present in LOD textures with 1-bit alpha with value of 2 or there will be "ghost outlines" on LOD textures when viewed from distance. Note that this flag must be present in texture file when binarizing model, because Binarize stores information about how to render textures in actual P3D file.
SWIZTAGG
- Optional
{ char "GGATZIWS"; ulong len; // 4 bytes ulong data; // 0x05040203 }
Swizzle is apparently used to modify texture components processing like swizzle modifiers in pixel shaders. For example ArmA sky texture has green channel stored in alpha channel and inversed to take advantage from feature that in DXT5 64 bits are used for alpha channel in each block and 64 bits for RBG, giving double the accuracy to green channel as opposed to storing texture just normally.
Exact format of swizzle data is still unknown. swizzle data:
char channelSwizzleA; char channelSwizzleR; char channelSwizzleG; char channelSwizzleB;
format of swizzle char:
bits 7-4 = 0; bit 3 - "1" flag. All channel data must be set to 0xff; bit 2 - "negate flag". Channel data must be negated. bit 1-0 - number of color channel: 00 - Alpha channel 01 - Red channel 10 - Green channel 11 - Blue channel
for example (*_nohq.paa textures)
swizzle data is:
0x05 - Alpha, Negated, stored in Red; 0x04 - Red, Negated, stored in Alpha; 0x02 - Green, Stored as is; 0x03 - Blue, Stored as is;
PROCTAGG
- Only ever seen with some DXT1 types
{ char "GGATCORP"; ulong len; // strlen(text) char text // NOT ASCIIz }
this is a non asciiz string (not zero terminated)
example:
x = ((u+1)*0.5); y = ((v+1)*0.5); if (y<0.5) then { sharpOut = 20; sharpIn = 60; offset = x-0.5; if (offset<0) then { edge = (1+offset)^sharpOut; } else { edge = (1-offset)^sharpIn; }; res = edge*y*2; } else { sharpOut = 20-40*(y-0.5); sharpIn = 60-125*(y-0.5); offset = x-0.5; if (offset<0) then { edge = (1+offset)^sharpOut; } else { edge = (1-offset)^sharpIn; }; res = edge; sdCoef = y * 2 - 1; shoreDisappear = 1 - sdCoef^2; res = res * shoreDisappear; }; a = 1; r = res; g = res; b = res;
OFFSTAGG
- Almost always present
{ char "GGATSFFO"; ulong len; // 16 * sizeof(ulong) ulong offsets[16]; }
Example: = 6 entries. last 10 unused 256 x 128 Size=16384 128 x 64 Size=4096 64 x 32 Size=1024 32 x 16 Size=256 16 x 8 Size=64 8 x 4 Size=16
MipMap data is presented in 'blocks'. One or more 'blocks' exist in a paa file.
This tag declares where each of these blocks are in the file, relative to start of file.
The location of each block is already known, relative to the size of the previous block (if any). So, although almost always present in paa files, it's use, is redundant.
This tag always contains 16 ULONG offsets. Each one is a hard offset to actual mipmap data relative to start of file.
Not all entries are used (obvuously) since most paa files contain less than 16 mipmaps. Unused offsets contain the value 0x00000000. There are no known examples of splattered offset entries. All offsets after the first 0 entry are 0 as well.
Palette
Palette { ushort nPaletteTriplets; // always 0 except for index palette type bytes BGR_Palette[nPaletteTriplets][3]; // only exists if nPaletteTriplets > 0 }
palette triplets when they exist, consist of BGR values
Mipmap
Mipmaps are in contiguous blocks that extend to end of file.
Mipmap { ushort width; // width of this mipmap ushort height; // height of this mipmap if (INDEX PALLETE) { if (width==0x4D2 && height==0x223D) // special 1234 x 8765 signature { ushort width; // actual width ushort height; // actual height } else { use Index Compression for this block } } if (width && height) { byte size[3]; // size of texture data in file. this is 24-bit unsigned integer. byte data[size]; // actual texture data compressed or otherwise } };
The last mipmap is a dummy consisting of zero width and height with NO FURTHER DATA
The size triplet reflects the size of data in the file not necessarily the actual size of the data.
Compression
ALL non DXT formats are unconditionally compressed using LZSS.
There are two wrinkles to LZSS compression not encountered in other uses of the same algorithm (pbo, p3d, wrp eg).
- The >1024 rule does not apply. ALL data is compressed. The end result is the smaller box sizes are most often larger on disk.
- The checksum following the data (all data sizes are acutally -4 their 'real' length) is SIGNED addititive. All other checksums are UNsigned additive.
example: Type: RGBA 4:4:4:4 texture AVGC: F5A9A9AB 256 x 128 Real Size 65536. Size in file 11770 128 x 64 Real Size 16384. Size in file 3521 64 x 32 Real Size 4096. Size in file 1148 32 x 16 Real Size 1024. Size in file 407 16 x 8 Real Size 256. Size in file 169 8 x 4 Real Size 64. Size in file 57 4 x 2 Real Size 16. Size in file 22 <<<<<
Taken as a combination, the real sizes (versus size on disk) are:
- DXT1 width*height/2 (same as the size triplet)
- DXT2 width*height (same as the size triplet)
- DXT3 width*height (same as the size triplet)
- DXT4 width*height (same as the size triplet)
- DXT5 width*height (same as the size triplet)
- RGB4 width*height*2
- RGB5 width*height*2
- RGB8 width*height*4
- GREY width*height*2
- INDEX width*height
Before ARMA2
DXT textures are stored "as is" in the file. Therefore the DXT data could be directly passed to the graphic hardware. For software decoding of DXT textures see DXTn compress/decompress
POST ARMA2
DXT formats are potentially compressed.
(All other formats remain the same with unconditional lzss compression)
ArmA2 introduces LZO compression for DXT. The criteria for DXT-LZO compression is having the top bit of the width paramater set.
Note that under this circumstance, the width bit must be masked before any size calculations
This bit is set when size on disk for dxt formats != expected size . (See above table). It is either a 'safety bit' for the unusual circumstance of size being same, or, simply a flag to indicate lzo compression.
Either way, there are (currently) no known instances of dxt compression where sizes are same, and quite obviously, all unequal sizes must, by defintion, be lzo compresssed.
Note also, that this is, truly, conditional compression. Only larger dimensions (256x256) eg are treated in this manner. Lesser value mipmaps in the same paa file, remain un altered.
Index Palette Compression
If the file doesn't start with a known Paa type the data array of the mipmaps contain the indices to the color palette (which appears just before mipmaps and only for palette type).
TWO (2) types of compression are in force determined by the special structure of index pallette mipmaps (see above)
- Standard LZSS.
if the signature for width and height IS 1234 x 8765 then standard lzss compression is used, using, the next four bytes as width and height.
otherwise:
A very basic "compression", which even can make the data slightly bigger, is used. The data is splitt into blocks of the following structure:
struct block { byte flag; byte data[...]; }
The flag can be of two different types. If the most significant bit of flag is zero, it tells you how much bytes to read. If flag is 0x05 for example you have to read 6 bytes (always one more then the value of flag). If the most significant bit of flag is one it tells you how often the next byte is repeated. If flag is 0x82 for example the value after the flag byte is repeated 3 times (always one more than (flagvalue - 0x80)).
Note that with this, special, compression, there is no checksum.
Index palette Mipmaps can have mixtures of both types.
Addenda
Decompression Code
see Compressed LZSS File Format
HexDump
The 0x4747 format mentioned in this picture indicates index palette format, because 0x4747 is the "GG" of "GGAT" signature.
Alpha channel interpolation
These two images visualize difference between alpha channel interpolation (FLAGTAGG header tag value).
FLAGTAGG = 1, interpolated alpha channel (default behaviour)
FLAGTAGG = 2, alpha channel interpolation disabled
Bibliography
Feersum's original posting on BIS forums: Paa/pac texture format documentation MSDN documentation on DXT1 textures: DirectX: Opaque and 1-Bit Alpha Textures Squish Compression DXTn compress/decompress