Let's Read .uasset Together
Hey everyone, have you been reading UAsset files?
This article is meant to help you become better friends with UAsset. It's a long article, so let's start with a bit of fun. Open the Content folder in your project and drag & drop a uasset file into the area below (everything is processed locally in your browser, so don't worry about anything being uploaded anywhere).
Drop a .uasset file here, or click to select
...Did you enjoy that? If you understand this article (and the engine implementation it covers), you might be able to create things like this yourself.
.uasset Overview
.uasset is Unreal Engine's asset file format. Virtually all assets that you import, create, or edit in the editor—Blueprints, Materials, StaticMeshes, etc.—are saved in this format.
In day-to-day development, the editor hides everything for you, so you never have to think about what's inside. UAsset might seem like some mysterious UE-specific file, but just like common file formats such as PNG or MP4, it contains well-structured data. If you know the specification, you can read it.
Environment
- Unreal Engine 5.3 - 5.7
This article covers uncooked (editor-editable) uasset files. Cooked assets after packaging have different property serialization methods, editor-only data is stripped, and the structure changes significantly. We won't cover .uexp separation or IoStore format here.
Why Understanding the UAsset Format is Useful
You might think—what's the point of knowing what's inside a uasset? It's true that you rarely need to think about it during normal development, but there are situations where this knowledge comes in handy.
- Asset Analysis
- When you want to extract specific information from large numbers of assets or investigate dependencies, understanding the format lets you build your own tools
- Compatibility Issue Investigation
- When you encounter assets that won't load between engine versions, examining the version information in the header helps isolate the cause
- Understanding Packaging
- Understanding how dependencies between assets are recorded gives you better insight into what gets included during cooking and packaging, and why certain assets end up in your build
- Simple Reading Outside UE
- When you want to check basic asset information (class, dependencies, etc.) without launching the editor
In this article, we'll explain the basic structure of uasset files. We'll reference engine source code like PackageFileSummary.h and LinkerLoad.cpp to get a grasp of the overall picture.
UAsset Fundamentals
Before diving into format details, we need to understand "what" uasset stores and "how" it stores it. This section organizes the internal engine concepts.
What is UPackage?
In Unreal Engine, assets are managed in units called Packages. One .uasset file corresponds to one package.
Packages are represented internally as the UPackage class. UPackage itself inherits from UObject, so it's handled using the same mechanisms as other objects.
UCLASS(MinimalAPI, Config=Engine)
class UPackage : public UObject
{
// Package flags
std::atomic<uint32> PackageFlagsPrivate;
// Path information for load source
FPackagePath LoadedPath;
// ...
};
Outer Hierarchy
A package contains multiple UObjects. These objects form a hierarchical structure through a parent-child relationship called Outer, where the topmost Outer is always UPackage.
For example, a Blueprint asset has a hierarchy like this:
UPackage "/Game/MyBlueprint"
└─ UBlueprint "MyBlueprint"
└─ UBlueprintGeneratedClass "MyBlueprint_C"
└─ UFunction "ExecuteUbergraph_MyBlueprint"
This hierarchical relationship is expressed in the file format through a field called OuterIndex.
Information Contained in a Package
When you import anything into UE, it becomes a uasset. PNG, FBX, WAV—they all become uasset. You might wonder what's actually inside.
Blueprints contain graph information, Textures contain image data, StaticMeshes contain geometry data—the content differs by asset type, but in fact, they're all expressed using common data structures.
Name Map
The foundation of everything is the Name Map. All FNames used within the package (class names, property names, object names, etc.) are stored here, and referenced by index from various locations.
Export
Objects defined within this package. Each export holds information like "what class is this an instance of," "what is the parent object," and "where is the serialized data." For Blueprints, the BP class itself and function objects are recorded as exports; for Textures, the texture object is recorded.
Import
References to external packages. The parent class being inherited from, referenced materials, other assets being used—things this package depends on are recorded as imports.
Bulk Data
Large binary data like texture pixel data or audio waveform data. This is managed separately from export serialization data and may be stored at the end of the file or in a separate file (.ubulk).
Other Metadata
In addition to these, dependency relationships between exports (Depends Map), asset registry information, and thumbnail images are also included.
Concrete Example: Blueprint Asset
When we actually parse BP_ThirdPersonCharacter.uasset from the UE5 ThirdPerson template, we find the following data:
- Name Map: 281 entries
- Import: 87 entries
- Export: 42 entries
Name Map (excerpt):
[ 0] /Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter
[ 1] /Script/CoreUObject
[ 2] /Script/Engine
[ 33] BP_ThirdPersonCharacter
[ 34] BP_ThirdPersonCharacter_C
Package paths, dependency package names, object names, property names—all strings are stored here.
Import (excerpt):
[ 40] /Script/CoreUObject.Class -> TP_ThirdPersonCharacter
[ 71] /Script/CoreUObject.Package -> /Script/Engine
[ 84] /Script/TP_ThirdPerson.TP_ThirdPersonCharacter -> Default__TP_ThirdPersonCharacter
Import 40 is a reference to the parent class TP_ThirdPersonCharacter, and Import 71 is a reference to the engine package.
Export (excerpt):
[ 1] BP_ThirdPersonCharacter : Blueprint (outer: root)
[ 2] BP_ThirdPersonCharacter_C : BlueprintGeneratedClass (outer: root)
[ 3] Default__BP_ThirdPersonCharacter_C : BP_ThirdPersonCharacter_C (outer: root)
[ 7] EventGraph : EdGraph (outer: BP_ThirdPersonCharacter)
[ 12] ExecuteUbergraph_BP_ThirdPersonCharacter : Function (outer: BP_ThirdPersonCharacter_C)
Export 1 is the Blueprint asset itself, Export 2 is the generated class, and Export 3 is the CDO (Class Default Object). EventGraph has Blueprint as its outer, forming the Outer hierarchy.
The important point here is that strings like "BP_ThirdPersonCharacter" are not stored directly. In the actual file, they're recorded as indices into the Name Map, and you need to reference the Name Map to get the name.
Version Compatibility
Unreal Engine is frequently updated, and the uasset format changes along with it. Types of changes include new property types, extensions to existing structures, and changes to serialization methods.
Officially, upgrading the engine "one version at a time" is recommended, but when you actually read the engine code, you'll find that uasset has a surprisingly well-thought-out version compatibility system.
The engine source defines VER_UE4_OLDEST_LOADABLE_PACKAGE = 214 as the oldest loadable version. Since the UE4 version enum has over 500 entries, 214 corresponds to early UE4. This means that even UE5.7 maintains compatibility to load assets from early UE4.
UAsset holds detailed version information in its header:
- Engine Version
- Version numbers for both UE4 and UE5
- Identifies which engine version saved the asset
- Custom Versions
- Version numbers independently maintained by each engine subsystem (animation, materials, physics, etc.)
- Managed with GUIDs as keys, allowing each subsystem to evolve independently
- Over 50 subsystems each have their own version
When loading an asset, the engine checks these version values and performs conversion from old formats to new formats as needed. The engine source is full of version branches like if (Ar.CustomVer(...) >= SomeVersion), maintaining compatibility over many years.
However, being able to read a uasset and having its contents work correctly are different things. If Blueprint nodes have been deprecated, material parameters have changed, or the code using the asset has changed, it may load but not work as expected. The "one version at a time" recommendation is probably meant to verify this semantic compatibility as you go.
UAsset File Format
Now that we understand what information uasset holds, let's see how it's actually stored in the file.
Overall Structure of a UAsset File
A uasset file has the following structure:
The position of each section is not fixed but determined by offset values stored in the header (Package File Summary). This design allows backward compatibility to be maintained even when sections are added in version updates.
This article won't exhaustively cover all sections (that's not realistic given Unreal Engine's scale). Instead, we'll focus on Package File Summary, Name Map, Import Map, and Export Map as the foundation for understanding uasset, while also explaining Thumbnail and Bulk Data.
Package File Summary (Header)
The Package File Summary is like the table of contents of a uasset file. Located at the beginning of the file, it holds metadata about the entire package (version information, package flags, etc.) and offsets to each subsequent section.
When loading a uasset, you first parse this header to understand the overall structure, then jump to the necessary sections to read data. In the engine, it's defined as the FPackageFileSummary struct, with serialization processing in PackageFileSummary.cpp.
Binary Layout
To get a feel for it, let's look at the actual binary layout. Conditional fields may or may not be present depending on version and flags.
| # | Size | Field | Description |
|---|---|---|---|
| 1 | 4 | Tag | Magic number (0x9E2A83C1) |
| 2 | 4 | LegacyFileVersion | Format version (-9 is latest) |
| 3 | 4 | LegacyUE3Version | For UE3 compatibility (condition: ≠-4) |
| 4 | 4 | FileVersionUE4 | UE4 version number |
| 5 | 4 | FileVersionUE5 | UE5 version number (condition: ≤-8) |
| 6 | 4 | FileVersionLicenseeUE | Licensee version |
| 7 | 20 | SavedHash | Package hash (UE5.1+) |
| 8 | 4 | TotalHeaderSize | Total header size |
| 9 | Variable | CustomVersions | Per-subsystem versions |
| 10 | Variable | PackageName | Package name (deprecated) |
| 11 | 4 | PackageFlags | Package flags |
| 12 | 4+4 | NameCount, NameOffset | Name Map info |
| 13 | 4+4 | ExportCount, ExportOffset | Export Map info |
| 14 | 4+4 | ImportCount, ImportOffset | Import Map info |
| ... | ... | ... | ... |
Full Field List (44 items)
| # | Size | Field | Description | Condition |
|---|---|---|---|---|
| 1 | 4 | Tag | Magic number (0x9E2A83C1) | |
| 2 | 4 | LegacyFileVersion | Format version (-9 is latest) | |
| 3 | 4 | LegacyUE3Version | For UE3 compatibility | ≠ -4 |
| 4 | 4 | FileVersionUE4 | UE4 version number | |
| 5 | 4 | FileVersionUE5 | UE5 version number | ≤ -8 |
| 6 | 4 | FileVersionLicenseeUE | Licensee version | |
| 7 | 20 | SavedHash | Package hash | UE5.1+ |
| 8 | 4 | TotalHeaderSize | Total header size | UE5.1+ |
| 9 | Variable | CustomVersions | Per-subsystem versions | ≤ -2 |
| 10 | 4 | TotalHeaderSize | Total header size | Before UE5.1 |
| 11 | Variable | PackageName | Package name (deprecated) | |
| 12 | 4 | PackageFlags | Package flags | |
| 13 | 4+4 | NameCount, NameOffset | Name Map count and offset | |
| 14 | 4+4 | SoftObjectPathsCount, Offset | Soft object path references | UE5.1+ |
| 15 | Variable | LocalizationId | Localization ID | UE4.14+ |
| 16 | 4+4 | GatherableTextDataCount, Offset | Gatherable text data | UE4.7+ |
| 17 | 4+4 | ExportCount, ExportOffset | Export Map count and offset | |
| 18 | 4+4 | ImportCount, ImportOffset | Import Map count and offset | |
| 19 | 4×4 | CellExport/ImportCount, Offset | Verse cell info | UE5.5+ |
| 20 | 4 | MetaDataOffset | Metadata offset | UE5.4+ |
| 21 | 4 | DependsOffset | Depends Map offset | |
| 22 | 4+4 | SoftPackageRefsCount, Offset | Soft package references | UE4.13+ |
| 23 | 4 | SearchableNamesOffset | Searchable names offset | UE4.15+ |
| 24 | 4 | ThumbnailTableOffset | Thumbnail table offset | |
| 25 | 4+4 | ImportTypeHierarchiesCount, Offset | Import type hierarchy info | UE5.7+ |
| 26 | 16 | Guid | Package GUID | Before UE5.1 |
| 27 | 16 | PersistentGuid | Persistent GUID | Editor |
| 28 | 4 | GenerationCount | Generation info count | |
| 29 | Variable | Generations[] | Past version info | |
| 30 | Variable | SavedByEngineVersion | Engine version at save time | UE4.5+ |
| 31 | Variable | CompatibleWithEngineVersion | Compatible engine version | UE4.12+ |
| 32 | 4 | CompressionFlags | Compression flags | |
| 33 | Variable | CompressedChunks[] | Compressed chunks (always empty) | |
| 34 | 4 | PackageSource | Package source identifier | |
| 35 | Variable | AdditionalPackagesToCook[] | Additional cook targets (always empty) | |
| 36 | 4 | NumTextureAllocations | Texture allocation count (always 0) | > -7 |
| 37 | 4 | AssetRegistryDataOffset | Asset registry data offset | |
| 38 | 8 | BulkDataStartOffset | Bulk data start offset | |
| 39 | 4 | WorldTileInfoDataOffset | World tile info offset | UE4.11+ |
| 40 | Variable | ChunkIDs[] | Streaming chunk IDs | UE4.10+ |
| 41 | 4+4 | PreloadDependencyCount, Offset | Preload dependencies | UE4.18+ |
| 42 | 4 | NamesReferencedFromExportDataCount | Names referenced from export data | UE5.0+ |
| 43 | 8 | PayloadTocOffset | Payload TOC offset | UE5.0+ |
| 44 | 4 | DataResourceOffset | Data resource offset | UE5.3+ |
The CustomVersions structure is "Count (i32) + (GUID 16bytes + Version i32) × Count". FString is "Length (i32) + UTF-8/UTF-16 string".
In actual parsing, you read through fields with conditional branches based on version values. Looking at operator<< in PackageFileSummary.cpp really shows how messy this gets. It's surprising how much code for UE3 format still remains.
Magic Number (Tag)
A magic number is a fixed byte sequence placed at the beginning of a file to identify the file format. Many file formats use this—PNG's 89 50 4E 47, PDF's 25 50 44 46, etc.—and programs use it to determine the format when reading a file.
The first 4 bytes of a uasset file are defined as a magic number with these values:
#define PACKAGE_FILE_TAG 0x9E2A83C1
#define PACKAGE_FILE_TAG_SWAPPED 0xC1832A9E // Byte order reversed
In the engine's FLinkerLoad::LoadPackageInternal, this magic number is read first to determine if it's a valid package file. PACKAGE_FILE_TAG_SWAPPED is provided to detect files saved with different endianness.
Version Information
As mentioned in the overview, uasset is strongly version-compatibility conscious. The header contains version information, and the engine branches processing based on these values when loading.
| Field | Description |
|---|---|
| LegacyFileVersion | Negative integer. Smaller values are newer (-4, -7, -8, etc.). -8 or below for UE5 |
| FileVersionUE4 | UE4 version number. Always serialized even for UE5 assets |
| FileVersionUE5 | UE5 version number (1000 or above). 0 for UE4 assets |
| CustomVersions | Custom versions per subsystem (array of GUID + version number) |
FileVersionUE4 and FileVersionUE5 are combined in the FPackageFileVersion struct, allowing you to determine which engine version saved the asset.
The reason FileVersionUE4 is serialized even for UE5 assets is for compatibility with existing version checks. There are many checks throughout the engine in the format if (Version >= VER_UE4_XXX), and for UE5 assets, this field is set to the final UE4 version value. This way, all UE4 version checks automatically pass, and UE5-specific features can be checked with FileVersionUE5.
struct FPackageFileVersion
{
// UE4 version
int32 FileVersionUE4 = 0;
// UE5 version (1000 or above is UE5)
int32 FileVersionUE5 = 0;
};
Let's also look at how CustomVersions is specifically used.
// Custom version for Editor subsystem
const FGuid FEditorObjectVersion::GUID(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41);
FDevVersionRegistration GRegisterEditorObjectVersion(
FEditorObjectVersion::GUID,
FEditorObjectVersion::LatestVersion,
TEXT("Dev-Editor")
);
Some notable CustomVersions include:
| CustomVersion | Purpose |
|---|---|
| FEditorObjectVersion | Editor features in general (FText, SplineComponent, etc.) |
| FReleaseObjectVersion | Changes for release builds |
| FAnimationCustomVersion | Animation-related |
| FSkeletalMeshCustomVersion | SkeletalMesh structural changes |
| FLandscapeCustomVersion | Landscape-related |
Let's look at an actual usage example with FText serialization.
Ar.UsingCustomVersion(FEditorObjectVersion::GUID);
if (Ar.CustomVer(FEditorObjectVersion::GUID) >= FEditorObjectVersion::TextFormatArgumentDataIsVariant)
{
// Read/write in newer format
Record << SA_VALUE(TEXT("Type"), TypeAsByte);
}
Ar.CustomVer() retrieves the CustomVersion recorded in the package, and processing branches based on whether it's at or above a specific version.
Package Flags
UPackage has various flags that represent the nature of the package.
| Flag | Description |
|---|---|
| PKG_ContainsMap | Package contains ULevel/UWorld |
| PKG_Cooked | Cooked (packaged) |
| PKG_EditorOnly | Editor-only package |
| PKG_CompiledIn | Package generated from C++ code (/Script/Engine, etc.) |
| PKG_NewlyCreated | Newly created package (unsaved) |
Key Offset Fields
The header stores offsets to each section. Here are the main ones:
| Field | Description |
|---|---|
| TotalHeaderSize | Total header size |
| NameCount / NameOffset | Name Map entry count and offset |
| ImportCount / ImportOffset | Import Map entry count and offset |
| ExportCount / ExportOffset | Export Map entry count and offset |
| DependsOffset | Offset to Depends Map |
| SoftPackageReferencesCount / Offset | Soft package reference info |
| ThumbnailTableOffset | Offset to thumbnail table |
| AssetRegistryDataOffset | Offset to asset registry data |
| BulkDataStartOffset | Bulk data start position |
The engine uses these offsets to jump to and read from the necessary sections.
Field Evolution Across Versions
An important note here is that header fields increase and decrease across versions. The engine source defines constants as EUnrealEngineObjectUE5Version.
enum EUnrealEngineObjectUE5Version
{
INITIAL_VERSION = 1000,
NAMES_REFERENCED_FROM_EXPORT_DATA = 1001,
PAYLOAD_TOC = 1002,
// ...
ADD_SOFTOBJECTPATH_LIST = 1008,
// ...
METADATA_SERIALIZATION_OFFSET = 1014,
VERSE_CELLS = 1015,
PACKAGE_SAVED_HASH = 1016,
};
For example, fields added in UE5 include:
| Version | Added Fields |
|---|---|
| 1008 (ADD_SOFTOBJECTPATH_LIST) | SoftObjectPathsCount, SoftObjectPathsOffset |
| 1014 (METADATA_SERIALIZATION_OFFSET) | MetaDataOffset |
| 1015 (VERSE_CELLS) | CellExportCount/Offset, CellImportCount/Offset |
| 1016 (PACKAGE_SAVED_HASH) | SavedHash (changed from GUID to FIoHash) |
The engine checks these version values to determine field presence and branches serialization processing accordingly. Looking at these reveals not just the format's history but also gives you a peek at names of features yet to be added—kind of interesting.
Name Map
The Name Map is a table that aggregates all names used within the package (class names, property names, object names, etc.). Throughout the file, instead of embedding strings directly, indices into the Name Map are recorded, eliminating duplicates and reducing file size.
Difference Between In-File FName and Runtime FName
FName in the file is fundamentally different from runtime FName.
Runtime FName:
- Index into a global NameTable shared across the entire engine
- Initialized at process startup,
NAME_Noneis always index 0 - Strings are stored only once and shared across all objects
In-File FName:
- Index into a NameMap independent to each package
- The same string can have different indices in different packages
- The file records local indices unrelated to the global table
// FName reference is 8 bytes
int32 NameIndex; // Index into package-local NameMap
int32 Number; // Instance number (for distinguishing same-named objects)
The Number field is used to distinguish multiple objects with the same name. For example, names like MyActor_0, MyActor_1 reference the same Name Map entry but are distinguished by Number (Number 0 means no suffix, 1 or above means _0, _1, ...).
Note that this is different from FNameEntrySerialized, which is used to serialize Name Map entries themselves. FNameEntrySerialized is a struct containing the actual string and hash values, used for parsing the Name Map.
Index Conversion During Load
When loading a package, the Linker converts file indices to runtime indices with the following process:
FNameEntrySerialized NameEntry(ENAME_LinkerConstructor);
for (int32 Idx = 0; Idx < NameCount; ++Idx)
{
*this << NameEntry;
// Register the string read from file as a global FName,
// and store its global index in NameMap
NameMap.Emplace(FName(NameEntry).GetDisplayIndex());
}
This NameMap array functions as a mapping table. To resolve the name at file index N, reference NameMap[N] and use the global FNameEntryId stored there to retrieve the string from the global table.
Name Map Serialization
The Name Map is serialized as an array of strings.
Each entry is serialized as a length-prefixed string (FString). From a certain UE4 version onward (VER_UE4_NAME_HASHES_SERIALIZED), hash values are also serialized together.
// String body
FString Name;
Ar << Name;
// Hash values (for newer versions)
if (Ar.UEVer() >= VER_UE4_NAME_HASHES_SERIALIZED)
{
uint16 DummyHashes[2];
Ar << DummyHashes[0] << DummyHashes[1];
}
An important point is that the first entry in the Name Map (index 0) is not necessarily "None". In the runtime global table, NAME_None is always index 0, but since the in-file Name Map is independent per package, the position of "None" varies by package.
Import Map
The Import Map is a list of external objects this package depends on. References to parent classes being inherited, materials being referenced, textures being used—objects existing in other packages are recorded here.
For example, if a Blueprint asset inherits from the Actor class, a reference to /Script/Engine.Actor is recorded as an Import.
Import Map Binary Layout
The Import Map consists of Summary.ImportCount entries arranged consecutively starting from the position Summary.ImportOffset. Each entry has the following layout:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 8 | ClassPackage | Package name containing the class (FName) |
| 8 | 8 | ClassName | Class name (FName) |
| 16 | 4 | OuterIndex | Reference to Outer (FPackageIndex) |
| 20 | 8 | ObjectName | Object name (FName) |
| 28 | 8 | PackageName | Owning package name (Editor only, UE4.18+) |
| 36 | 4 | bImportOptional | Optional flag (UE5.1+) |
FName is 8 bytes as int32 NameIndex + int32 Number. FPackageIndex is 4 bytes as int32, where negative values point to Import and positive values point to Export. Bools are—perhaps surprisingly—serialized as 4-byte integers. This is a historical specification to maintain compatibility with the UBOOL type from UE3 and earlier.
Entry size varies by version, so it can't be treated as a fixed size. The engine serializes and reads each entry sequentially.
Imports can have hierarchical structure. For example, a reference to /Script/Engine.Actor is expressed as a chain of Import entries like this:
- Import 0: Package
/Script/Engine(OuterIndex = 0) - Import 1: Class
Actor(OuterIndex = references Import 0)
This OuterIndex is a type called FPackageIndex, a mechanism that can reference both Imports and Exports.
FPackageIndex
FPackageIndex is an index pointing to an entry in either the Import Map or Export Map. It's a wrapper around int32 that distinguishes the reference target by the sign of the value.
// Relationship between FPackageIndex value and reference target
// 0 : null (points nowhere)
// Negative: Index into Import Map (-1 → Import[0], -2 → Import[1], ...)
// Positive: Index into Export Map (1 → Export[0], 2 → Export[1], ...)
bool IsNull() const { return Index == 0; }
bool IsImport() const { return Index < 0; }
bool IsExport() const { return Index > 0; }
int32 ToImport() const { return -Index - 1; } // Import[0] is -1
int32 ToExport() const { return Index - 1; } // Export[0] is 1
This mechanism allows object references to be handled uniformly whether they're from external packages or within the same package. Simple, but a nice design.
Export Map
The Export Map is a list of objects defined within this package. While Import is "reference to external," Export is "what this file provides."
For Blueprint assets, the Blueprint class itself, default objects, and function graphs are recorded as Exports.
Export Map Binary Layout
The Export Map consists of Summary.ExportCount entries arranged consecutively starting from the position Summary.ExportOffset. It has more fields than Import Map and varies more significantly across versions.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | ClassIndex | Reference to class (FPackageIndex) |
| 4 | 4 | SuperIndex | Reference to super class (UStruct only) |
| 8 | 4 | TemplateIndex | Reference to template (UE4.14+) |
| 12 | 4 | OuterIndex | Reference to Outer |
| 16 | 8 | ObjectName | Object name (FName) |
| 24 | 4 | ObjectFlags | Object flags |
| 28 | 8 | SerialSize | Size of serialized data |
| 36 | 8 | SerialOffset | Position of serialized data |
| 44 | 4 | bForcedExport | Forced export flag |
| 48 | 4 | bNotForClient | Exclude from client flag |
| 52 | 4 | bNotForServer | Exclude from server flag |
| 56 | 4 | PackageFlags | Package flags |
| 60 | 4 | bNotAlwaysLoadedForEditorGame | Not always loaded for editor game flag |
| 64 | 4 | bIsAsset | Asset flag |
| ... | ... | ... | (Dependency info, etc., added in UE4.17+) |
SerialSize and SerialOffset were 4 bytes (int32) before UE4.14. The Export Map stores only object metadata; the actual property data is stored separately at the position indicated by SerialOffset.
bIsAsset Flag
FObjectExport has a flag called bIsAsset that indicates whether that export is an object displayed in the Content Browser (for Blueprints, the BP itself; for Materials, the material itself).
for (const FObjectExport& Export : ExportMap)
{
if (Export.bIsAsset)
{
// Object displayed in Content Browser
}
}
An export with null (0) OuterIndex means "object directly under UPackage," but Blueprint packages can have multiple top-level objects (Blueprint itself, BlueprintGeneratedClass, CDO, etc.). bIsAsset is a flag to identify the representative object that appears in the Content Browser among these.
Accessing Serialized Data
Each Export indicates the position and size of actual object data through SerialOffset and SerialSize. By reading the byte sequence in this range, you can access the object's properties and internal data.
However, interpreting this byte sequence requires type-specific processing. Since it gets quite complex from here, this article won't cover it.
Class Path Resolution
Now that we've seen the structure of Import Map, Export Map, and Name Map, we can combine them to resolve object class paths.
For example, if an export's ClassIndex points to Import[3], and the Import structure is:
Import[2]: ClassName="Package", ObjectName="/Script/Engine", OuterIndex=0
Import[3]: ClassName="Class", ObjectName="Actor", OuterIndex=-3 (→Import[2])
In this case, the class path is /Script/Engine.Actor. By recursively following OuterIndex and resolving names from the Name Map to concatenate them, you can build the complete path.
FString ResolveImportPath(int32 ImportIndex)
{
const FObjectImport& Import = ImportMap[ImportIndex];
FString Name = NameMap[Import.ObjectName.Index];
if (Import.OuterIndex.IsNull())
{
return Name;
}
else if (Import.OuterIndex.IsImport())
{
FString OuterPath = ResolveImportPath(Import.OuterIndex.ToImport());
return OuterPath + TEXT(".") + Name;
}
return Name;
}
Thumbnail
The thumbnail images displayed in the Content Browser are embedded within the uasset file.
Here's something interesting: these thumbnails are actually the result of rendering the asset in the editor, saved directly as PNG or JPEG. Since it's a standard image format rather than GPU-specific encoding, you can extract the relevant portion from the uasset and display it as-is.
Multiple Thumbnails
Thumbnails can be saved for each export object within a package. For typical assets (Blueprint, Material, etc.) there's only one for the main object, but the format allows storing multiple thumbnails.
Thumbnail Storage Location
At the position pointed to by ThumbnailTableOffset is the thumbnail index (table of contents), with the image data for each thumbnail stored before it.
Thumbnail Table Structure
The Thumbnail Table consists of two parts: the index and the image data.
[Thumbnail image data 0]
[Thumbnail image data 1]
...
[Position pointed to by ThumbnailTableOffset]
├─ Count (int32)
├─ Entry 0: ObjectClass, ObjectPath, FileOffset
├─ Entry 1: ObjectClass, ObjectPath, FileOffset
└─ ...
Each index entry has the following structure:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | Variable | ObjectClass | Object's class name (FString) |
| Variable | Variable | ObjectPath | Object's path (FString) |
| Variable | 4 | FileOffset | Image data offset within file |
Thumbnail Image Data
Each thumbnail's image data is serialized as FObjectThumbnail.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | ImageWidth | Image width |
| 4 | 4 | ImageHeight | Image height (negative means JPEG compression) |
| 8 | 4 | DataLength | Length of compressed data |
| 12 | Variable | CompressedData | PNG or JPEG compressed image data |
If ImageHeight is negative, it indicates JPEG compression; positive indicates PNG compression. The actual height is the absolute value.
Only CompressedImageData (already compressed image) is saved to file; the decompressed pixel data ImageData is not written to disk. CompressedImageData is a byte sequence that can be decoded directly as PNG/JPEG.
Bulk Data
Some assets contain large amounts of binary data, such as texture mipmaps or audio waveforms. Such data is managed through a mechanism called Bulk Data.
Why Separate Bulk Data?
With normal property serialization, all data is expanded into memory when loading an object. However, loading all of a huge binary like texture pixel data every time is inefficient.
Bulk Data solves this problem by separating and managing the payload (actual data).
- Lazy Loading: Load only the Summary and Export Map first; payload isn't loaded until needed
- Streaming: Load only necessary parts with async IO (texture streaming, etc.)
- Memory Mapping: Map the file directly to memory, leveraging the OS paging mechanism
Relationship Between Exports and Bulk Data
During export serialization, asset-specific code calls FBulkData::Serialize to write Bulk Data. For example, UTexture2D serializes FBulkData for each mipmap.
At this time, there are two options for where to store the Bulk Data payload:
| Storage Location | Description |
|---|---|
| Inline | Write payload on the spot during export serialization |
| End of File | Write only metadata during serialization; payload is gathered after BulkDataStartOffset |
With inline, the payload exists within the Export Data region as part of the export data. With end of file, only metadata (flags, size, offset) is recorded in Export Data, and the actual data is stored in a separate region.
Also, for Uncooked Assets (Editor Assets), the payload is usually stored at the end of the file, but when cooked, it may be separated into a separate file like .ubulk.
Content Virtualization
When Content Virtualization (Virtual Assets) is enabled, Bulk Data payloads are stored in a Package Trailer format at the end of the .uasset file, and those payloads can be moved (virtualized) to external backends (Perforce, DDC, etc.). Virtualized payloads are downloaded on-demand when needed.
Bulk Data Metadata
Each Bulk Data has metadata indicating the payload's position and size.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | BulkDataFlags | Flags |
| 4 | 4 or 8 | ElementCount | Element count |
| 8 or 12 | 4 or 8 | SizeOnDisk | Size on disk |
| 12 or 20 | 8 | OffsetInFile | Offset within file |
The size of ElementCount and SizeOnDisk changes depending on the presence of the BULKDATA_Size64Bit flag.
Key Flags
| Flag | Value | Description |
|---|---|---|
BULKDATA_PayloadAtEndOfFile | 0x0001 | Stored at end of file |
BULKDATA_SerializeCompressedZLIB | 0x0002 | ZLIB compressed |
BULKDATA_ForceInlinePayload | 0x0040 | Force inline storage |
BULKDATA_Size64Bit | 0x2000 | Size fields are 64-bit |
The actual payload byte sequence has different formats depending on the asset type, making generic parsing difficult.
Conclusion
This article explained the basic structure of uasset files.
- UPackage and Outer Hierarchy: uasset is a serialized UPackage file that preserves the parent-child relationships (Outer) of UObjects
- Section Structure: Files consist of multiple sections, with offsets to each section recorded in the Package File Summary (header)
- Version Compatibility: FileVersionUE4/UE5 and CustomVersions handle format changes over many years
- Name Map: Aggregates all strings within the package, eliminating duplicates by referencing via index from various locations
- Import Map / Export Map: Import is references to external packages, Export is objects defined within the package. Uniformly referenced via FPackageIndex
- Thumbnail: Thumbnail images are embedded directly in PNG/JPEG format and can be extracted and displayed
- Bulk Data: Large binary data is separated for lazy loading, streaming, and memory mapping
Understanding this structure should deepen your understanding of how the engine's serialization processing works. If you've read this article, you should now have a good understanding of what the items displayed in the "playground" at the beginning of the article mean.
This was an attempt to explain UAsset as a file format. While there's information about struct serialization out there, information about uasset itself is surprisingly scarce, so I hope this proves useful.
There's enough content about Export internal serialization (Tagged Property, Native, Binary, etc.) to fill another article of similar length, so I'd like to write about it if the opportunity arises.