Skip to content

Commit

Permalink
Unload render assets from RAM (#10520)
Browse files Browse the repository at this point in the history
# Objective
- No point in keeping Meshes/Images in RAM once they're going to be sent
to the GPU, and kept in VRAM. This saves a _significant_ amount of
memory (several GBs) on scenes like bistro.
- References
  - #1782
  - #8624 

## Solution
- Augment RenderAsset with the capability to unload the underlying asset
after extracting to the render world.
- Mesh/Image now have a cpu_persistent_access field. If this field is
RenderAssetPersistencePolicy::Unload, the asset will be unloaded from
Assets<T>.
- A new AssetEvent is sent upon dropping the last strong handle for the
asset, which signals to the RenderAsset to remove the GPU version of the
asset.

---

## Changelog
- Added `AssetEvent::NoLongerUsed` and
`AssetEvent::is_no_longer_used()`. This event is sent when the last
strong handle of an asset is dropped.
- Rewrote the API for `RenderAsset` to allow for unloading the asset
data from the CPU.
- Added `RenderAssetPersistencePolicy`.
- Added `Mesh::cpu_persistent_access` for memory savings when the asset
is not needed except for on the GPU.
- Added `Image::cpu_persistent_access` for memory savings when the asset
is not needed except for on the GPU.
- Added `ImageLoaderSettings::cpu_persistent_access`.
- Added `ExrTextureLoaderSettings`.
- Added `HdrTextureLoaderSettings`.

## Migration Guide
- Asset loaders (GLTF, etc) now load meshes and textures without
`cpu_persistent_access`. These assets will be removed from
`Assets<Mesh>` and `Assets<Image>` once `RenderAssets<Mesh>` and
`RenderAssets<Image>` contain the GPU versions of these assets, in order
to reduce memory usage. If you require access to the asset data from the
CPU in future frames after the GLTF asset has been loaded, modify all
dependent `Mesh` and `Image` assets and set `cpu_persistent_access` to
`RenderAssetPersistencePolicy::Keep`.
- `Mesh` now requires a new `cpu_persistent_access` field. Set it to
`RenderAssetPersistencePolicy::Keep` to mimic the previous behavior.
- `Image` now requires a new `cpu_persistent_access` field. Set it to
`RenderAssetPersistencePolicy::Keep` to mimic the previous behavior.
- `MorphTargetImage::new()` now requires a new `cpu_persistent_access`
parameter. Set it to `RenderAssetPersistencePolicy::Keep` to mimic the
previous behavior.
- `DynamicTextureAtlasBuilder::add_texture()` now requires that the
`TextureAtlas` you pass has an `Image` with `cpu_persistent_access:
RenderAssetPersistencePolicy::Keep`. Ensure you construct the image
properly for the texture atlas.
- The `RenderAsset` trait has significantly changed, and requires
adapting your existing implementations.
  - The trait now requires `Clone`.
- The `ExtractedAsset` associated type has been removed (the type itself
is now extracted).
  - The signature of `prepare_asset()` is slightly different
- A new `persistence_policy()` method is now required (return
RenderAssetPersistencePolicy::Unload to match the previous behavior).
- Match on the new `NoLongerUsed` variant for exhaustive matches of
`AssetEvent`.
  • Loading branch information
JMS55 committed Jan 3, 2024
1 parent 0275508 commit 4442439
Show file tree
Hide file tree
Showing 46 changed files with 495 additions and 256 deletions.
26 changes: 15 additions & 11 deletions crates/bevy_asset/src/assets.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::{self as bevy_asset, LoadState};
use crate::{Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, UntypedHandle};
use crate::{self as bevy_asset};
use crate::{
Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, LoadState, UntypedHandle,
};
use bevy_ecs::{
prelude::EventWriter,
system::{Res, ResMut, Resource},
Expand Down Expand Up @@ -484,9 +486,7 @@ impl<A: Asset> Assets<A> {
}

/// A system that synchronizes the state of assets in this collection with the [`AssetServer`]. This manages
/// [`Handle`] drop events and adds queued [`AssetEvent`] values to their [`Events`] resource.
///
/// [`Events`]: bevy_ecs::event::Events
/// [`Handle`] drop events.
pub fn track_assets(mut assets: ResMut<Self>, asset_server: Res<AssetServer>) {
let assets = &mut *assets;
// note that we must hold this lock for the entire duration of this function to ensure
Expand All @@ -496,24 +496,28 @@ impl<A: Asset> Assets<A> {
let mut infos = asset_server.data.infos.write();
let mut not_ready = Vec::new();
while let Ok(drop_event) = assets.handle_provider.drop_receiver.try_recv() {
let id = drop_event.id;
let id = drop_event.id.typed();

assets.queued_events.push(AssetEvent::Unused { id });

if drop_event.asset_server_managed {
let untyped = id.untyped(TypeId::of::<A>());
if let Some(info) = infos.get(untyped) {
let untyped_id = drop_event.id.untyped(TypeId::of::<A>());
if let Some(info) = infos.get(untyped_id) {
if info.load_state == LoadState::Loading
|| info.load_state == LoadState::NotLoaded
{
not_ready.push(drop_event);
continue;
}
}
if infos.process_handle_drop(untyped) {
assets.remove_dropped(id.typed());
if infos.process_handle_drop(untyped_id) {
assets.remove_dropped(id);
}
} else {
assets.remove_dropped(id.typed());
assets.remove_dropped(id);
}
}

// TODO: this is _extremely_ inefficient find a better fix
// This will also loop failed assets indefinitely. Is that ok?
for event in not_ready {
Expand Down
9 changes: 9 additions & 0 deletions crates/bevy_asset/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub enum AssetEvent<A: Asset> {
Modified { id: AssetId<A> },
/// Emitted whenever an [`Asset`] is removed.
Removed { id: AssetId<A> },
/// Emitted when the last [`super::Handle::Strong`] of an [`Asset`] is dropped.
Unused { id: AssetId<A> },
/// Emitted whenever an [`Asset`] has been fully loaded (including its dependencies and all "recursive dependencies").
LoadedWithDependencies { id: AssetId<A> },
}
Expand All @@ -35,6 +37,11 @@ impl<A: Asset> AssetEvent<A> {
pub fn is_removed(&self, asset_id: impl Into<AssetId<A>>) -> bool {
matches!(self, AssetEvent::Removed { id } if *id == asset_id.into())
}

/// Returns `true` if this event is [`AssetEvent::Unused`] and matches the given `id`.
pub fn is_unused(&self, asset_id: impl Into<AssetId<A>>) -> bool {
matches!(self, AssetEvent::Unused { id } if *id == asset_id.into())
}
}

impl<A: Asset> Clone for AssetEvent<A> {
Expand All @@ -51,6 +58,7 @@ impl<A: Asset> Debug for AssetEvent<A> {
Self::Added { id } => f.debug_struct("Added").field("id", id).finish(),
Self::Modified { id } => f.debug_struct("Modified").field("id", id).finish(),
Self::Removed { id } => f.debug_struct("Removed").field("id", id).finish(),
Self::Unused { id } => f.debug_struct("Unused").field("id", id).finish(),
Self::LoadedWithDependencies { id } => f
.debug_struct("LoadedWithDependencies")
.field("id", id)
Expand All @@ -65,6 +73,7 @@ impl<A: Asset> PartialEq for AssetEvent<A> {
(Self::Added { id: l_id }, Self::Added { id: r_id })
| (Self::Modified { id: l_id }, Self::Modified { id: r_id })
| (Self::Removed { id: l_id }, Self::Removed { id: r_id })
| (Self::Unused { id: l_id }, Self::Unused { id: r_id })
| (
Self::LoadedWithDependencies { id: l_id },
Self::LoadedWithDependencies { id: r_id },
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_asset/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ impl std::fmt::Debug for StrongHandle {
#[reflect(Component)]
pub enum Handle<A: Asset> {
/// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept
/// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata.
/// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata.
Strong(Arc<StrongHandle>),
/// A "weak" reference to an [`Asset`]. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`],
/// nor will it keep assets alive.
Expand Down Expand Up @@ -189,7 +189,7 @@ impl<A: Asset> Handle<A> {

/// Converts this [`Handle`] to an "untyped" / "generic-less" [`UntypedHandle`], which stores the [`Asset`] type information
/// _inside_ [`UntypedHandle`]. This will return [`UntypedHandle::Strong`] for [`Handle::Strong`] and [`UntypedHandle::Weak`] for
/// [`Handle::Weak`].
/// [`Handle::Weak`].
#[inline]
pub fn untyped(self) -> UntypedHandle {
self.into()
Expand Down
18 changes: 16 additions & 2 deletions crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ pub enum AssetMode {
///
/// When developing an app, you should enable the `asset_processor` cargo feature, which will run the asset processor at startup. This should generally
/// be used in combination with the `file_watcher` cargo feature, which enables hot-reloading of assets that have changed. When both features are enabled,
/// changes to "original/source assets" will be detected, the asset will be re-processed, and then the final processed asset will be hot-reloaded in the app.
/// changes to "original/source assets" will be detected, the asset will be re-processed, and then the final processed asset will be hot-reloaded in the app.
///
/// [`AssetMeta`]: meta::AssetMeta
/// [`AssetSource`]: io::AssetSource
Expand Down Expand Up @@ -872,13 +872,23 @@ mod tests {
id: id_results.d_id,
},
AssetEvent::Modified { id: a_id },
AssetEvent::Unused { id: a_id },
AssetEvent::Removed { id: a_id },
AssetEvent::Unused {
id: id_results.b_id,
},
AssetEvent::Removed {
id: id_results.b_id,
},
AssetEvent::Unused {
id: id_results.c_id,
},
AssetEvent::Removed {
id: id_results.c_id,
},
AssetEvent::Unused {
id: id_results.d_id,
},
AssetEvent::Removed {
id: id_results.d_id,
},
Expand Down Expand Up @@ -1062,7 +1072,11 @@ mod tests {
// remove event is emitted
app.update();
let events = std::mem::take(&mut app.world.resource_mut::<StoredEvents>().0);
let expected_events = vec![AssetEvent::Added { id }, AssetEvent::Removed { id }];
let expected_events = vec![
AssetEvent::Added { id },
AssetEvent::Unused { id },
AssetEvent::Removed { id },
];
assert_eq!(events, expected_events);

let dep_handle = app.world.resource::<AssetServer>().load(dep_path);
Expand Down
4 changes: 3 additions & 1 deletion crates/bevy_core_pipeline/src/tonemapping/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use bevy_reflect::Reflect;
use bevy_render::camera::Camera;
use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin};
use bevy_render::extract_resource::{ExtractResource, ExtractResourcePlugin};
use bevy_render::render_asset::RenderAssets;
use bevy_render::render_asset::{RenderAssetPersistencePolicy, RenderAssets};
use bevy_render::render_resource::binding_types::{
sampler, texture_2d, texture_3d, uniform_buffer,
};
Expand Down Expand Up @@ -356,6 +356,7 @@ fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image {
CompressedImageFormats::NONE,
false,
image_sampler,
RenderAssetPersistencePolicy::Unload,
)
.unwrap()
}
Expand All @@ -381,5 +382,6 @@ pub fn lut_placeholder() -> Image {
},
sampler: ImageSampler::Default,
texture_view_descriptor: None,
cpu_persistent_access: RenderAssetPersistencePolicy::Unload,
}
}
24 changes: 12 additions & 12 deletions crates/bevy_gizmos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ use bevy_render::{
color::Color,
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
primitives::Aabb,
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
render_asset::{
PrepareAssetError, RenderAsset, RenderAssetPersistencePolicy, RenderAssetPlugin,
RenderAssets,
},
render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass},
render_resource::{
binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout,
Expand Down Expand Up @@ -365,28 +368,25 @@ struct GpuLineGizmo {
}

impl RenderAsset for LineGizmo {
type ExtractedAsset = LineGizmo;

type PreparedAsset = GpuLineGizmo;

type Param = SRes<RenderDevice>;

fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
fn persistence_policy(&self) -> RenderAssetPersistencePolicy {
RenderAssetPersistencePolicy::Unload
}

fn prepare_asset(
line_gizmo: Self::ExtractedAsset,
self,
render_device: &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
let position_buffer_data = cast_slice(&line_gizmo.positions);
) -> Result<Self::PreparedAsset, PrepareAssetError<Self>> {
let position_buffer_data = cast_slice(&self.positions);
let position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
usage: BufferUsages::VERTEX,
label: Some("LineGizmo Position Buffer"),
contents: position_buffer_data,
});

let color_buffer_data = cast_slice(&line_gizmo.colors);
let color_buffer_data = cast_slice(&self.colors);
let color_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
usage: BufferUsages::VERTEX,
label: Some("LineGizmo Color Buffer"),
Expand All @@ -396,8 +396,8 @@ impl RenderAsset for LineGizmo {
Ok(GpuLineGizmo {
position_buffer,
color_buffer,
vertex_count: line_gizmo.positions.len() as u32,
strip: line_gizmo.strip,
vertex_count: self.positions.len() as u32,
strip: self.strip,
})
}
}
Expand Down
8 changes: 6 additions & 2 deletions crates/bevy_gltf/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use bevy_render::{
},
prelude::SpatialBundle,
primitives::Aabb,
render_asset::RenderAssetPersistencePolicy,
render_resource::{Face, PrimitiveTopology},
texture::{
CompressedImageFormats, Image, ImageAddressMode, ImageFilterMode, ImageLoaderSettings,
Expand Down Expand Up @@ -120,7 +121,7 @@ pub struct GltfLoader {
/// |s: &mut GltfLoaderSettings| {
/// s.load_cameras = false;
/// }
/// );
/// );
/// ```
#[derive(Serialize, Deserialize)]
pub struct GltfLoaderSettings {
Expand Down Expand Up @@ -389,7 +390,7 @@ async fn load_gltf<'a, 'b, 'c>(
let primitive_label = primitive_label(&gltf_mesh, &primitive);
let primitive_topology = get_primitive_topology(primitive.mode())?;

let mut mesh = Mesh::new(primitive_topology);
let mut mesh = Mesh::new(primitive_topology, RenderAssetPersistencePolicy::Unload);

// Read vertex attributes
for (semantic, accessor) in primitive.attributes() {
Expand Down Expand Up @@ -433,6 +434,7 @@ async fn load_gltf<'a, 'b, 'c>(
let morph_target_image = MorphTargetImage::new(
morph_target_reader.map(PrimitiveMorphAttributesIter),
mesh.count_vertices(),
RenderAssetPersistencePolicy::Unload,
)?;
let handle =
load_context.add_labeled_asset(morph_targets_label, morph_target_image.0);
Expand Down Expand Up @@ -724,6 +726,7 @@ async fn load_image<'a, 'b>(
supported_compressed_formats,
is_srgb,
ImageSampler::Descriptor(sampler_descriptor),
RenderAssetPersistencePolicy::Unload,
)?;
Ok(ImageOrPath::Image {
image,
Expand All @@ -745,6 +748,7 @@ async fn load_image<'a, 'b>(
supported_compressed_formats,
is_srgb,
ImageSampler::Descriptor(sampler_descriptor),
RenderAssetPersistencePolicy::Unload,
)?,
label: texture_label(&gltf_texture),
})
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,7 @@ pub fn extract_materials<M: Material>(
let mut changed_assets = HashSet::default();
let mut removed = Vec::new();
for event in events.read() {
#[allow(clippy::match_same_arms)]
match event {
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
changed_assets.insert(*id);
Expand All @@ -830,6 +831,7 @@ pub fn extract_materials<M: Material>(
changed_assets.remove(id);
removed.push(*id);
}
AssetEvent::Unused { .. } => {}
AssetEvent::LoadedWithDependencies { .. } => {
// TODO: handle this
}
Expand Down
Loading

0 comments on commit 4442439

Please sign in to comment.