Skip to content

PVM Texture Archive

The PVM format is a container format used on the Sega Dreamcast to store multiple PVR texture files. It’s commonly used in Dreamcast games like Ikaruga to package related textures together for efficient loading and memory management.

File Structure Overview

A PVM file consists of a header section followed by multiple PVR textures:

+----------------+
| PVMH Header |
+----------------+
| Texture Info |
+----------------+
| PVRT Texture 1 |
+----------------+
| PVRT Texture 2 |
+----------------+
| ... |
+----------------+

PVMH Header

The PVM file begins with a header that contains information about the contained textures:

// PVM Header Structure
typedef struct {
char magic[4]; // "PVMH" Magic identifier
uint32_t header_size; // Size of the header section
uint16_t flags; // Format flags
uint16_t texture_count; // Number of textures in file
// Followed by texture entries
} PVMHeader;

Flag Bits Explained

The flags field in the header controls what information is stored for each texture entry:

  • 0x01: When set, each texture entry has a global index value
  • 0x02: When set, each entry contains texture dimensions
  • 0x04: When set, each entry contains format information
  • 0x08: When set, each entry contains a filename (up to 28 characters)

Texture Entries

After the header, there’s a list of texture entries. The content of each entry depends on the flags:

// Example texture entry with all flags set
typedef struct {
uint16_t index; // Texture index (always present)
char name[28]; // Texture name (if flags & 0x08)
uint16_t format; // Format information (if flags & 0x04)
uint16_t size; // Dimensions (if flags & 0x02)
uint32_t global_index; // Global index (if flags & 0x01)
} TextureEntry;

Size Field Decoding

When dimensions are included (flags & 0x02), the size field encodes both width and height:

width = 1 << ((size & 0x0f) + 2)
height = 1 << (((size >> 4) & 0x0f) + 2)

For example:

  • If size = 0x34, then width = 1 << (4 + 2) = 64 and height = 1 << (3 + 2) = 32

PVRT Texture Data

After the texture entries, the actual texture data follows as a series of PVRT blocks. Each PVRT block contains:

+----------------+
| "PVRT" Magic | (4 bytes)
+----------------+
| Data Size | (4 bytes)
+----------------+
| Texture Header | (8 bytes)
+----------------+
| Texture Data | (variable size)
+----------------+

Example PVM Structure

Here’s a hex dump visualization of a simple PVM file with two textures:

Offset | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | ASCII
--------+-------------------------------------------------+----------------
0000000 | 50 56 4D 48 xx xx xx xx 0F 00 02 00 00 00 xx xx | PVMH........
0000010 | 54 45 58 54 55 52 45 5F 31 00 00 00 00 00 00 00 | TEXTURE_1.....
0000020 | 00 00 00 00 00 00 00 00 00 00 00 00 01 00 43 00 | ..............
0000030 | 00 00 00 00 01 00 xx xx xx xx xx xx xx xx xx xx | ..............
0000040 | 54 45 58 54 55 52 45 5F 32 00 00 00 00 00 00 00 | TEXTURE_2.....
0000050 | 00 00 00 00 00 00 00 00 00 00 00 00 01 00 43 00 | ..............
0000060 | 01 00 00 00 50 56 52 54 xx xx xx xx xx xx xx xx | ....PVRT......
...

Decoding Process

The process to decode a PVM file is as follows:

  1. Read the PVMH header and validate the magic number
  2. Read the header size and flags
  3. Based on flags, read the texture entries
  4. For each texture:
    • Locate the PVRT block by searching for “PVRT” magic
    • Read the PVRT data size
    • Decode the PVR texture (see PVR Format for details)

Programmatic Access

Here’s an example of how to parse a PVM file header in Python:

import struct
def parse_pvm_header(file_path):
with open(file_path, 'rb') as f:
# Read magic number
magic = f.read(4)
if magic != b'PVMH':
raise ValueError("Not a valid PVM file")
# Read header size
header_size = struct.unpack('I', f.read(4))[0]
# Read flags and texture count
flags, tex_count = struct.unpack('HH', f.read(4))
textures = []
for i in range(tex_count):
# Read texture index
index = struct.unpack('H', f.read(2))[0]
texture = {'index': index}
# Read texture name if flag is set
if flags & 0x08:
name_bytes = f.read(28)
name = name_bytes.decode().replace('\x00', '')
texture['name'] = name
# Read format if flag is set
if flags & 0x04:
format_value = struct.unpack('H', f.read(2))[0]
texture['format'] = format_value >> 8
# Read dimensions if flag is set
if flags & 0x02:
size = struct.unpack('H', f.read(2))[0]
width = 1 << ((size & 0x0f) + 2)
height = 1 << (((size >> 4) & 0x0f) + 2)
texture['width'] = width
texture['height'] = height
# Read global index if flag is set
if flags & 0x01:
global_index = struct.unpack('I', f.read(4))[0]
texture['global_index'] = global_index
textures.append(texture)
return {
'header_size': header_size,
'flags': flags,
'texture_count': tex_count,
'textures': textures
}

Common Usage Patterns

In Dreamcast games like Ikaruga, PVM files are typically used to:

  1. Group related textures - For example, all textures for a single 3D model
  2. Organize by level - Level-specific textures packed together for efficient loading
  3. Maintain animation frames - Sequential frames stored in a single PVM
  4. Interface elements - Menu and HUD textures grouped into PMVs by UI screen

Relationship with NJ Models

In the context of Ikaruga and other Dreamcast games, PVM files are often paired with NJ model files:

  • SHIP.NJ - The 3D model file
  • SHIP.PVM - The associated textures for the model

This pairing allows the game engine to efficiently load both the geometry and textures for a game object.