Assets & Resource Management¶
AssetServer¶
The AssetServer manages the loading, caching, and lifetime of all asset data. It is stored as a world resource via a shared_ptr to ensure it can be safely shared across systems and background loading threads.
Accessing the AssetServer¶
In a system, access the AssetServer using the Res or ResMut system parameters:
void my_system(Res<std::shared_ptr<AssetServer>> asset_server) {
// Use the server
auto mesh = (*asset_server)->load<MeshAsset>("Meshes/Hero.hlasset");
}
Custom Importers¶
Each asset type must have an importer registered. The AssetPlugin handles standard types, but you can register custom ones:
server->register_importer<MyAsset>([](const std::filesystem::path& path, AssetServer& s) -> std::any {
// Load and return MyAsset
return MyAsset{};
});
server->register_extensions<MyAsset>({"myasset", "hvemy"});
Sub-asset Creation¶
Complex assets, like a GLB model, often contain multiple internal sub-assets (e.g., embedded textures, materials). To manage these properly, importers can use server.store() and server.add_dependency().
server.store(path, asset): Manually registers an asset that doesn't have its own file. Thepathis used as a unique cache key (often the parent's path + a name). Returns a rawAssetHandle.server.add_dependency(parent, child): Tells the server that thechildasset's lifetime is tied to theparent. When the parent's refcount reaches zero, it will also release its reference to the child.
Example from a GLB Importer:
// 1. Create sub-asset (e.g., embedded texture)
TextureAsset tex = load_from_buffer(glb_data);
// 2. Store it manually in the server
AssetHandle tex_handle = server.store("Models/Hero.glb::Texture_0", std::move(tex));
// 3. Link child's lifetime to the parent mesh
server.add_dependency(mesh_handle, tex_handle);
Loading Assets¶
Async Loading (Non-blocking):
Returns a Handle<T> immediately. The asset is loaded on a background thread.
Sync Loading (Blocking): Blocks the current thread until the asset is loaded.
Diverse Loading Examples¶
The AssetServer can load any registered type. Common examples:
// Textures (PNG, JPG, HDR)
Handle<TextureAsset> albedo = server->load<TextureAsset>("Textures/Grass_Albedo.png");
// Audio (WAV, OGG, MP3)
Handle<AudioData> music = server->load<AudioData>("Music/MainTheme.ogg");
Loading Batches¶
Use a LoadBatch to track the loading status of multiple assets. This is ideal for loading a scene or a set of resources for a specific entity.
// 1. Start a batch
auto builder = server->load_batch();
// 2. Add multiple assets (all types supported)
builder.add<MeshAsset>("Meshes/Hero.hlasset")
.add<TextureAsset>("Textures/Hero_Base.png")
.add<AudioData>("Sfx/Hero_Spawn.wav");
// 3. Submit and get a tracker
LoadBatch batch = builder.submit();
// 4. Check progress or completion
if (batch.is_complete()) {
float percent = batch.progress() * 100.0f;
int remaining = batch.remaining();
}
- Reference Counting: The
LoadBatchholds a strong reference to all assets within it. When the batch is destroyed, those references are released unless you've stored the handles elsewhere. - Progress Tracking:
batch.progress()returns afloat(0.0 to 1.0) indicating the percentage of assets that have either finished loading or failed.
Status and Resolution¶
Check the status of an asset or resolve a handle to its underlying data:
AssetStatus status = server->status(mesh.untyped());
if (server->is_loaded(mesh.untyped())) {
const MeshAsset* data = server->get<MeshAsset>(mesh.untyped());
// Use data...
}
Handle\<T>¶
A Handle<T> is a reference-counted smart pointer to an asset. Under the hood, it wraps a raw AssetHandle.
AssetHandle¶
The AssetHandle is a 64-bit packed ID consisting of:
- 32-bit Index: Points to the asset's slot in the internal storage array.
- 32-bit Generation: Incremented each time a slot is reused, preventing "dangling" handles to destroyed assets.
Because it is a simple 64-bit POD (Plain Old Data) type, it is extremely efficient to copy, pass, and store. It contains no pointers, making it safe for serialization and use in multi-threaded contexts.
- Acquire/Release:
Handle<T>automatically increments the refcount on construction/copy and decrements on destruction. - Untyped Access: Use
handle.untyped()to get the rawAssetHandle(e.g., for internal engine APIs or serialization). - Null Checks: Handles can be checked for validity:
if (mesh) { ... }.
Binary Asset Format (.hlasset)¶
Helios uses a unified binary format (.hlasset) for all imported assets. This format is optimized for fast loading and contains a GUID-based header.
Header Structure¶
All .hlasset files start with a fixed-size header followed by variable-length metadata and the payload:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 8 | Magic | ASSET_BINARY_MAGIC ("HLASSET\0") |
| 8 | 16 | GUID | Unique identifier (two uint64_t) |
| 24 | 4 | Type | AssetBinaryType enum |
| 28 | 4 | Version | Format version (currently 1) |
| 32 | 4 | Meta Count | Number of metadata entries |
| ... | ... | Metadata | Key-value pairs (length-prefixed strings) |
| ... | 4 | Data Size | Size of the payload |
| ... | D | Payload | Raw asset data |
Asset Types¶
| Type | Default Extensions |
|---|---|
| Mesh | .hlasset, .gltf, .glb |
| Texture | .hlasset, .png, .jpg, .hdr |
| Audio | .hlasset, .wav, .ogg |
| Shader | .hlasset, .spv |
| Scene | .hlasset, .hvescn |
Import & Export Pipeline¶
The AssetServer provides a pipeline for converting source files (like .glb or .png) into optimized .hlasset binaries.
Importing¶
Importing a source file generates a new .hlasset file with a unique GUID:
std::string hlasset_path = server->import_asset(
"Source/Models/Hero.glb",
AssetBinaryType::Mesh,
"Meshes/Hero.hlasset"
);
Re-importing¶
If a source file changes, you can re-import it while preserving the original GUID:
if (server->is_source_outdated("Meshes/Hero.hlasset")) {
server->reimport_asset("Meshes/Hero.hlasset", "Source/Models/Hero_V2.glb");
}
Garbage Collection¶
Assets are automatically garbage-collected when their reference count reaches zero. The AssetPlugin adds a system to PreUpdate that triggers this:
You can also explicitly unload an asset: