Skip to content

PVR Texture Format

The PVR (PowerVR) texture format is the native texture format used by the Sega Dreamcast’s PowerVR2 (CLX2) graphics hardware. This format was optimized for the specific architecture of the PowerVR GPU, providing efficient texture data storage and fast rendering performance.

GBIX Header Extension

Some PVR files in Dreamcast games (including Ikaruga) contain an additional GBIX header section before the standard PVRT section. This extension was commonly used to associate textures with a global identifier.

GBIX Structure

+----------------+
| "GBIX" | 4 bytes - Magic identifier
+----------------+
| Section Size | 4 bytes - Size of GBIX data (typically 8)
+----------------+
| Global Index | 4 bytes - Texture global identifier
+----------------+
| (padding) | Variable - To align to 8 bytes
+----------------+
| "PVRT" | 4 bytes - Standard PVR magic identifier
+----------------+
| ... PVR data ...
+----------------+

The Global Index value is often used by the game engine to:

  • Track and reference textures in memory
  • Associate textures with specific models or objects
  • Handle texture caching and replacement

When working with PVR files that contain a GBIX header, parsers must skip this section (typically 16 bytes total) before processing the actual PVRT data.

File Structure

A PVR file consists of a header followed by texture data:

+-----------------+
| Magic "PVRT" | 4 bytes
+-----------------+
| Data Size | 4 bytes
+-----------------+
| Texture Header | 8 bytes
+-----------------+
| [Palette Data] | Optional
+-----------------+
| [VQ Codebook] | Optional
+-----------------+
| Texture Data | Variable size
+-----------------+

Color Formats

PVR textures support several pixel formats, each offering different color depths and alpha channel options:

ValueFormatDescriptionBPP
0x00ARGB15551-bit alpha, 5-bit RGB16
0x01RGB565No alpha, 5-bit R/B, 6-bit G16
0x02ARGB44444-bit alpha, 4-bit RGB16
0x03YUV422YUV format for video textures16
0x04BUMPBump mapping format16
0x05RGB555No alpha, 5-bit RGB (1 bit unused)16
0x06ARGB88888-bit alpha, 8-bit RGB32
0x06YUV420Alternate YUV format (same value as ARGB8888)16

Data Formats

The data format determines how the texture is stored and accessed:

ValueFormatDescription
0x01TWIDDLEDMorton order layout for optimal cache usage
0x02TWIDDLED_MMTwiddled with mipmaps
0x03VQVector Quantized compression
0x04VQ_MMVector Quantized with mipmaps
0x05PALETTIZE44-bit indexed with palette
0x06PALETTIZE4_MM4-bit indexed with palette and mipmaps
0x07PALETTIZE88-bit indexed with palette
0x08PALETTIZE8_MM8-bit indexed with palette and mipmaps
0x09RECTANGLENon-square, non-twiddled
0x0BSTRIDERectangular with stride
0x0DTWIDDLED_RECTANGLERectangular texture stored as twiddled squares
0x0EABGRDirect ABGR format
0x0FABGR_MMDirect ABGR with mipmaps
0x10SMALLVQSmall codebook VQ compressed
0x11SMALLVQ_MMSmall codebook VQ with mipmaps
0x12TWIDDLED_MM_ALIASAlternative twiddled mipmap format

Morton Order (“Twiddled”) Format

“Twiddled” textures are stored in Morton order (Z-order curve), which interleaves the bits of the X and Y coordinates. This layout optimizes texture access patterns for the PowerVR’s tile-based rendering architecture.

// Function to convert from linear to Morton order
uint32_t toMorton(uint16_t x, uint16_t y) {
uint32_t morton = 0;
for (int i = 0; i < 16; i++) {
morton |= ((x & (1 << i)) << i) | ((y & (1 << i)) << (i + 1));
}
return morton;
}

Dreamcast PVR Detwiddling Visualizer

20px
0
1
2
3
4
5
6
7
16
17
18
19
20
21
22
23
64
65
66
67
68
69
70
71
80
81
82
83
84
85
86
87
8
9
10
11
12
13
14
15
24
25
26
27
28
29
30
31
72
73
74
75
76
77
78
79
88
89
90
91
92
93
94
95
32
33
34
35
36
37
38
39
48
49
50
51
52
53
54
55
96
97
98
99
100
101
102
103
112
113
114
115
116
117
118
119
40
41
42
43
44
45
46
47
56
57
58
59
60
61
62
63
104
105
106
107
108
109
110
111
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
144
145
146
147
148
149
150
151
192
193
194
195
196
197
198
199
208
209
210
211
212
213
214
215
136
137
138
139
140
141
142
143
152
153
154
155
156
157
158
159
200
201
202
203
204
205
206
207
216
217
218
219
220
221
222
223
160
161
162
163
164
165
166
167
176
177
178
179
180
181
182
183
224
225
226
227
228
229
230
231
240
241
242
243
244
245
246
247
168
169
170
171
172
173
174
175
184
185
186
187
188
189
190
191
232
233
234
235
236
237
238
239
248
249
250
251
252
253
254
255

About Dreamcast PVR Detwiddling

The Dreamcast PowerVR graphics hardware stores textures in memory using a "twiddled" format, which follows a Z-order (Morton order) curve. This visualization shows the memory address order of each pixel in the twiddled format.

To detwiddle a texture, you would read the texture data sequentially from memory and place each pixel at the position indicated by these numbers to reconstruct the original image.

This approach optimizes texture cache coherence by keeping spatially local pixels close together in memory.

Vector Quantization (VQ)

VQ compression is a block-based compression technique used in PVR textures. It works by:

  1. Dividing the texture into 2×2 pixel blocks (16 bytes each in ARGB8888)
  2. Creating a codebook of common/representative blocks
  3. Storing an index into this codebook for each 2×2 block in the texture

This can achieve compression ratios of 8:1 or better while maintaining reasonable visual quality.

+------------------------+
| Codebook Size (256) | Implicit - not stored in file
+------------------------+
| Codebook (256 entries) | Each entry is a 2×2 pixel block
+------------------------+
| Indices | One byte per 2×2 block in texture
+------------------------+

Mipmapping

PVR textures with mipmaps include multiple versions of the texture at different resolutions. Each mipmap level is half the width and height of the previous level, down to 1×1 pixel.

Mipmapped formats include:

  • TWIDDLED_MM
  • VQ_MM
  • PALETTIZE4_MM
  • PALETTIZE8_MM
  • ABGR_MM
  • SMALLVQ_MM
  • TWIDDLED_MM_ALIAS

Palettized Formats

Palettized textures store a color palette (lookup table) followed by indices into that palette for each pixel:

+------------------------+
| 16-entry Palette | 16 colors in the specified pixel format
+------------------------+
| 4-bit Indices | One nibble per pixel (packed 2 per byte)
+------------------------+

Code Examples

1. Reading a PVR Header

#include <stdio.h>
#include <stdint.h>
#include <string.h>
typedef struct {
uint8_t pixel_format;
uint8_t data_format;
uint8_t padding[2];
uint16_t width;
uint16_t height;
} PVRTextureHeader;
void read_pvr_header(const char* filename) {
FILE* file = fopen(filename, "rb");
if (!file) {
printf("Failed to open file\n");
return;
}
// Check for PVRT magic
char magic[4];
fread(magic, 1, 4, file);
if (memcmp(magic, "PVRT", 4) != 0) {
printf("Not a valid PVR file\n");
fclose(file);
return;
}
// Skip data size
fseek(file, 4, SEEK_CUR);
// Read texture header
PVRTextureHeader header;
fread(&header, sizeof(header), 1, file);
printf("PVR Texture Info:\n");
printf(" Dimensions: %d×%d\n", header.width, header.height);
printf(" Pixel Format: 0x%02X\n", header.pixel_format);
printf(" Data Format: 0x%02X\n", header.data_format);
fclose(file);
}

2. Converting Between Color Formats

// Convert ARGB1555 to RGBA8888
void argb1555_to_rgba8888(uint16_t input, uint8_t* output) {
uint8_t a = (input & 0x8000) ? 0xFF : 0x00;
uint8_t r = ((input >> 10) & 0x1F) << 3;
uint8_t g = ((input >> 5) & 0x1F) << 3;
uint8_t b = (input & 0x1F) << 3;
// Fill in the low bits (copy from high bits)
r |= r >> 5;
g |= g >> 5;
b |= b >> 5;
output[0] = r;
output[1] = g;
output[2] = b;
output[3] = a;
}
// Convert RGB565 to RGBA8888
void rgb565_to_rgba8888(uint16_t input, uint8_t* output) {
uint8_t r = ((input >> 11) & 0x1F) << 3;
uint8_t g = ((input >> 5) & 0x3F) << 2;
uint8_t b = (input & 0x1F) << 3;
// Fill in the low bits (copy from high bits)
r |= r >> 5;
g |= g >> 6;
b |= b >> 5;
output[0] = r;
output[1] = g;
output[2] = b;
output[3] = 0xFF; // No alpha in RGB565
}
// Convert ARGB4444 to RGBA8888
void argb4444_to_rgba8888(uint16_t input, uint8_t* output) {
uint8_t a = ((input >> 12) & 0xF) << 4;
uint8_t r = ((input >> 8) & 0xF) << 4;
uint8_t g = ((input >> 4) & 0xF) << 4;
uint8_t b = (input & 0xF) << 4;
// Fill in the low bits (copy from high bits)
a |= a >> 4;
r |= r >> 4;
g |= g >> 4;
b |= b >> 4;
output[0] = r;
output[1] = g;
output[2] = b;
output[3] = a;
}

3. Untwiddle Function

// Convert twiddled coordinates to linear
uint32_t untwiddle(uint16_t x, uint16_t y) {
uint32_t morton = 0;
for (int i = 0; i < 16; i++) {
morton |= ((x & (1 << i)) << i) | ((y & (1 << i)) << (i + 1));
}
return morton;
}
// Decode a twiddled texture (simple example)
void decode_twiddled_texture(const uint16_t* src, uint32_t* dst, int width, int height) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int morton_idx = untwiddle(x, y);
// Check bounds and untwiddle
if (morton_idx < width * height) {
uint16_t color = src[morton_idx];
// Convert to RGBA8888 (example using ARGB1555)
uint8_t rgba[4];
argb1555_to_rgba8888(color, rgba);
// Write to destination (assuming RGBA8888 output)
dst[y * width + x] = (rgba[3] << 24) | (rgba[0] << 16) | (rgba[1] << 8) | rgba[2];
}
}
}
}

Special Cases and Limitations

  • Power-of-Two: Most PVR formats require power-of-two texture dimensions
  • Square Textures: Some formats (particularly twiddled) work best with square textures
  • Mipmapping: Not all hardware can filter mipmaps correctly
  • Small VQ: The SMALLVQ variant uses smaller codebooks for small textures:
    • 16×16 or smaller: 16 codebook entries
    • 32×32: 32 codebook entries
    • 64×64: 128 codebook entries
    • Larger: 256 codebook entries