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 Structuretypedef 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;
flags bit field meanings:0x01 - Has global indices0x02 - Has dimensions0x04 - Has formats0x08 - Has file names
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 settypedef 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:
- Read the PVMH header and validate the magic number
- Read the header size and flags
- Based on flags, read the texture entries
- 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:
- Group related textures - For example, all textures for a single 3D model
- Organize by level - Level-specific textures packed together for efficient loading
- Maintain animation frames - Sequential frames stored in a single PVM
- 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 fileSHIP.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.