The Asset Pipeline and Handles
Every GPU resource in pebble — meshes, textures, materials, computes — follows the same unified CPU→GPU asset model. Understanding this once means every other rendering page in this book looks the same.
The pattern
- Build a CPU-side description (
Mesh::new(...),Texture::from_file(...),Material::new(...), …). - Call
.build_asset(name, &mut assets)to insert it into that type’sAssets<T>and get back aHandle<T>. - Some time later — usually the very next
AssetSyncstage — the asset finishes uploading to the GPU. Until then, lookups on the handle returnNone.
fn setup(backend: Read<Backend>, mut meshes: Write<Assets<Mesh>>) {
let handle: Handle<Mesh> = Mesh::new(vertices, indices).build_asset("player", &mut meshes);
// stash `handle` in a component or resource
}
fn use_it(meshes: Read<Assets<Mesh>>, handle: Read<Handle<Mesh>>) {
if let Some(gpu_mesh) = meshes.get(*handle) {
// ready to draw
}
}
Handle<T> is Copy, Eq, Hash — cheap to store in a component or use as a map key. A stale handle (its entry removed) just yields None, never panics.
Assets<T>
insert(name, source)— as above, via each type’sbuild_asset.get(handle)— the GPU-side processed value, once ready.get_source(handle)/get_source_mut(handle)— the CPU-side source (e.g. a mesh’s raw vertices, for building a collision shape from the same data).is_ready(handle)— whether the upload has finished.mark_dirty(handle)— re-queues an entry for re-upload, e.g. after mutating it viaget_source_mut.get_by_name/get_source_by_name/get_handle_by_name— same lookups, keyed by the name string instead of a handle.iter()— every currently-loaded asset of this type, source data included, ready or not.
Why uploads retry instead of failing
Asset<B>::upload returns Option<Self::Processed> — None just means “try again next tick,” not an error. This is what lets you build a mesh before the GPU backend exists, or a material that depends on a shared bind group layout that hasn’t been registered yet, without hand-rolled ordering: the asset system keeps retrying every AssetSync tick until every dependency (declared via Asset::Deps) is available, then uploads once and stops.
Defining your own asset type
AssetSource/Asset<B> are the two traits every built-in type implements. The asset! macro is a pure syntax transform that writes both at once:
asset!(MyThing => GPUMyThing, |self, backend: &Backend| {
Some(GPUMyThing { /* ... */ })
});
// or, with dependencies on other resources:
asset!(MyThing => GPUMyThing, deps: [SomePool], |self, backend: &Backend, deps| {
Some(GPUMyThing { /* ... */ })
});
Then register it the same way every built-in type is registered:
app.add_plugin(AssetPlugin::<Backend, MyThing>::new())