# Godot Resource Loading Pipeline and PackedData Explained: From Request to Binary

> Explore Godot's resource loading pipeline and PackedData. Learn how ResourceLoader handles assets with threading and caching, and how PackedData efficiently serializes untyped data.

- Repository: [Godot Engine/godot](https://github.com/godotengine/godot)
- Tags: internals
- Published: 2026-02-26

---

**Godot loads any asset through the `ResourceLoader` façade, which dispatches requests to specialized `ResourceFormatLoader` implementations with automatic threading, caching, and dependency resolution, while `PackedDataContainer` provides a binary serialization format for untyped `Array` and `Dictionary` data using offset-based addressing and string deduplication.**

The `godotengine/godot` repository implements a multi-stage asset management system designed to handle complex dependency trees and threaded I/O operations. Understanding the **resource loading pipeline** is critical for optimizing game startup times and extending the engine with custom formats, while the **PackedData** system offers an efficient alternative to text-based serialization for generic data structures. This guide examines the exact C++ implementation to demonstrate how these systems operate under the hood.

## ResourceLoader: The Central Dispatcher

At the heart of Godot's asset system lies **`ResourceLoader`**, defined in [`core/io/resource_loader.h`](https://github.com/godotengine/godot/blob/main/core/io/resource_loader.h) and implemented in [`core/io/resource_loader.cpp`](https://github.com/godotengine/godot/blob/main/core/io/resource_loader.cpp). This singleton class acts as the unified entry point for all resource requests, exposing methods like `load(path, type_hint, cache_mode, ...)` and `load_threaded_request()`.

Rather than handling file formats directly, `ResourceLoader` maintains a stack of **`ResourceFormatLoader`** implementations registered via `add_resource_format_loader()`. When a load request arrives, the dispatcher iterates through the `loader[]` array and selects the first loader whose `recognize_path()` method returns `true` for the given file extension.

## The Resource Loading Pipeline Steps

The pipeline executes through a precise sequence of operations orchestrated in `ResourceLoader::_load_start()` and `_run_load_task()`:

1. **Request Initiation** – The public API `ResourceLoader::load()` accepts a path, optional type hint, and `CacheMode` enum (defaulting to `CACHE_MODE_REUSE`).

2. **Thread Allocation** – `_load_start()` creates a **`LoadToken`** and **`ThreadLoadTask`** structure. If called from a worker thread or explicitly requested, the task enters `LOAD_THREAD_DISTRIBUTE` mode; otherwise it runs synchronously on the calling thread.

3. **Path Resolution** – The system invokes `_validate_local_path()` to normalize `res://` prefixes, followed by `_path_remap()` for translation overrides and `ResourceUID::ensure_path()` for UID-based lookups.

4. **Loader Selection** – A linear search through the registered loaders identifies the appropriate `ResourceFormatLoader` via the virtual `recognize_path()` method.

5. **Cache Verification** – Before disk access, `ResourceCache::has()` checks for existing instances. If found and `CACHE_MODE_REUSE` is active, the cached `Ref<Resource>` returns immediately.

6. **Format-Specific Loading** – The selected loader's `_load(path, original_path, ...)` virtual method executes. For scene files, this delegates to `ResourceFormatLoaderText` in [`scene/resources/resource_format_text.cpp`](https://github.com/godotengine/godot/blob/main/scene/resources/resource_format_text.cpp); for images, `ResourceFormatLoaderImage` handles the decode.

7. **Dependency Resolution** – During parsing, loaders call `ResourceLoader::get_dependencies()` to recursively load sub-resources (textures, scripts, meshes). Progress aggregates across the dependency tree via `_dependency_get_progress()`, which sums completion percentages from `sub_tasks` sets.

8. **Cache Insertion** – Successful loads insert the new `Resource` into `ResourceCache` using `ResourceCache::get_ref()` and `ResourceCache::lock`, unless `CACHE_MODE_IGNORE` was specified.

9. **Signal Emission** – Optional `ResourceLoadedCallback` functions trigger, and queued resource-changed connections emit on the main thread after threaded loads complete.

10. **Reference Return** – The `Ref<Resource>` returns to the caller via `_load_complete()`, or `null` if any step failed.

## Thread Safety and Deadlock Prevention

Godot's threaded loading relies on **`thread_load_mutex`** (a `SafeBinaryMutex`) to protect the global task map. Each `ThreadLoadTask` contains a condition variable for synchronous waiting and tracks parent/child relationships to prevent memory leaks.

The system detects circular dependencies—where Resource A loads Resource B, which attempts to load Resource A—by marking tasks with `THREAD_LOAD_IN_PROGRESS`. When a cycle is detected, the engine allows temporary re-entrancy on the same thread rather than deadlocking, breaking the cycle while maintaining the `LoadToken` reference count.

## PackedDataContainer: Binary Serialization System

For scenarios requiring compact storage of untyped collections, Godot provides **`PackedDataContainer`** in [`core/io/packed_data_container.cpp`](https://github.com/godotengine/godot/blob/main/core/io/packed_data_container.cpp). This class serializes `Array` and `Dictionary` variants into a contiguous binary blob optimized for size and random access.

### Packing Logic and Binary Layout

The `pack(Variant data)` method accepts only `Array` or `Dictionary` types and initiates recursive packing via `_pack()`:

- Primitive types encode via `encode_variant()` directly into a temporary `Vector<uint8_t>` (`tmpdata`).
- Nested arrays and dictionaries store as **`PackedDataContainerRef`** instances containing 32-bit offsets into the parent blob rather than duplicating data.
- A `string_cache` hash map deduplicates identical strings, writing each unique string once and referencing it by index.

The resulting binary layout follows this structure:

| Offset | Content |
|--------|---------|
| 0 | Type marker (`0xFFFFFFFF` for Dictionary, `0xFFFFFFFE` for Array) |
| 4 | Entry count (32-bit integer) |
| 8 | For Arrays: 32-bit offsets to each entry. For Dictionaries: 12-byte hash-key-value triples |
| Variable | Serialized variant payloads |

### Reading and Iteration

`PackedDataContainer` implements the iterator protocol through `_iter_init_ofs()`, `_iter_next_ofs()`, and `_iter_get_ofs()`, allowing GDScript `for` loops to traverse packed data efficiently. Random access uses `_key_at_ofs()` to retrieve values by index or key, returning `PackedDataContainerRef` wrappers for nested containers that lazily decode sub-trees via their stored offsets.

## Practical Implementation Examples

### Loading a Texture with Caching

```gdscript

# Uses CACHE_MODE_REUSE by default; returns cached instance if available

var hero_texture : Texture2D = ResourceLoader.load("res://sprites/hero.png") as Texture2D
if hero_texture == null:
    push_error("Failed to load hero texture")

```

This call enters `ResourceLoader::load()`, which invokes `_validate_local_path()` for the `res://` prefix, locates the `ResourceFormatLoaderImage` via `recognize_path()`, and either returns a cached reference or decodes the PNG data through the loader's `_load()` implementation.

### Threaded Scene Loading in C++

```cpp
// Request async load
Error err = ResourceLoader::load_threaded_request("res://levels/cave.tscn");
if (err != OK) {
    ERR_PRINT("Threaded load failed to start");
}

// Poll for completion in _process()
float progress = 0.0;
ResourceLoader::ThreadLoadStatus status = ResourceLoader::load_threaded_get_status(
    "res://levels/cave.tscn", 
    &progress
);

if (status == ResourceLoader::THREAD_LOAD_LOADED) {
    Ref<PackedScene> scene = ResourceLoader::load_threaded_get("res://levels/cave.tscn");
    // Instance scene...
}

```

The `load_threaded_request()` function creates a `ThreadLoadTask` with `LOAD_THREAD_DISTRIBUTE`, scheduling `_run_load_task()` on a worker thread while the main thread continues execution.

### Serializing Game Data with PackedDataContainer

```gdscript
var player_data = {
    "name": "Aragoth",
    "level": 50,
    "inventory": ["sword", "shield", "potion"],
    "stats": {"str": 18, "dex": 14, "int": 10}
}

var packer = PackedDataContainer.new()
var error = packer.pack(player_data)
if error == OK:
    # Access internal binary data for storage or network transmission

    var bytes = packer.get("_PackedDataContainer__data")
    # Save to file or send via network...

    
    # Later, reconstruct:

    var unpacker = PackedDataContainer.new()
    unpacker.set("_PackedDataContainer__data", bytes)
    var restored = unpacker.getvar(0)  # Returns original Dictionary

```

The `pack()` method calls `_pack()` recursively, encoding the dictionary into the binary format described above. The `getvar(0)` method uses `_key_at_ofs()` to reconstruct the variant tree from offsets.

## Key Source Files

The following files contain the critical implementations referenced throughout this guide:

- **[`core/io/resource_loader.h`](https://github.com/godotengine/godot/blob/main/core/io/resource_loader.h)** – Class definition for `ResourceLoader`, `LoadToken`, and `ThreadLoadTask` structures
- **[`core/io/resource_loader.cpp`](https://github.com/godotengine/godot/blob/main/core/io/resource_loader.cpp)** – `_load_start()`, `_run_load_task()`, `_validate_local_path()`, and cache management logic
- **[`core/io/packed_data_container.h`](https://github.com/godotengine/godot/blob/main/core/io/packed_data_container.h)** – Declaration of `PackedDataContainer` and `PackedDataContainerRef`
- **[`core/io/packed_data_container.cpp`](https://github.com/godotengine/godot/blob/main/core/io/packed_data_container.cpp)** – Implementation of `_pack()`, `_key_at_ofs()`, and iterator protocol methods
- **[`scene/resources/resource_format_text.cpp`](https://github.com/godotengine/godot/blob/main/scene/resources/resource_format_text.cpp)** – Example `ResourceFormatLoader` implementation for `.tscn` and `.tres` files

## Summary

- **`ResourceLoader`** serves as the façade for all asset loading, delegating format-specific parsing to pluggable `ResourceFormatLoader` implementations.
- The pipeline supports **threaded loading** via `LoadToken` reference counting and `ThreadLoadTask` structures protected by `thread_load_mutex`.
- **Automatic dependency resolution** recursively loads sub-resources while aggregating progress across the dependency tree via `_dependency_get_progress()`.
- **`PackedDataContainer`** serializes untyped `Array` and `Dictionary` data into compact binary blobs with string deduplication and offset-based addressing.
- The container provides **O(1) random access** to nested elements through `PackedDataContainerRef` without fully deserializing the entire structure.

## Frequently Asked Questions

### How does Godot handle circular dependencies during resource loading?

Godot detects circular dependencies when a load attempt encounters a `THREAD_LOAD_IN_PROGRESS` status on the same task. Rather than deadlocking, the engine permits temporary re-entrancy on the calling thread, allowing the cyclic load to complete while maintaining thread safety through the `LoadToken` reference counting system.

### What is the difference between ResourceLoader.load() and preload() in GDScript?

`ResourceLoader.load()` invokes the full runtime pipeline with path remapping, caching checks, and potential threaded execution, making it suitable for dynamic loading. `preload()` is a compile-time constant loader that performs validation during script compilation and stores a direct reference, bypassing runtime path resolution and offering marginally faster access for static assets.

### Can I implement support for custom file formats in Godot?

Yes, by inheriting from `ResourceFormatLoader` and implementing `recognize_path()` to identify your extension and `_load()` to return a `Resource` instance. Register your loader via `ResourceLoader::add_resource_format_loader()` to integrate it into the standard pipeline, allowing `ResourceLoader.load()` to handle your custom format transparently.

### When should I use PackedDataContainer instead of JSON or ConfigFile?

Use **PackedDataContainer** when you need to store large, nested untyped data structures with minimal memory overhead and fast random access. Unlike JSON or ConfigFile, which parse text into tree structures, PackedData maintains data in a binary format with deduplicated strings and offset-based indexing, making it ideal for save files, network packets, or exported data tables where parsing speed and file size matter more than human readability.