mirror of
https://github.com/bevyengine/bevy
synced 2025-02-16 14:08:32 +00:00
Unload render assets from RAM (#10520)
# 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 - https://github.com/bevyengine/bevy/pull/1782 - https://github.com/bevyengine/bevy/pull/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`.
This commit is contained in:
parent
02755086e8
commit
44424391fe
46 changed files with 496 additions and 257 deletions
|
@ -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},
|
||||
|
@ -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
|
||||
|
@ -496,10 +496,13 @@ 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
|
||||
{
|
||||
|
@ -507,13 +510,14 @@ impl<A: Asset> Assets<A> {
|
|||
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 {
|
||||
|
|
|
@ -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> },
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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)
|
||||
|
@ -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 },
|
||||
|
|
|
@ -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.
|
||||
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
@ -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::Removed {
|
||||
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,
|
||||
},
|
||||
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
@ -356,6 +356,7 @@ fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image {
|
|||
CompressedImageFormats::NONE,
|
||||
false,
|
||||
image_sampler,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
@ -381,5 +382,6 @@ pub fn lut_placeholder() -> Image {
|
|||
},
|
||||
sampler: ImageSampler::Default,
|
||||
texture_view_descriptor: None,
|
||||
cpu_persistent_access: RenderAssetPersistencePolicy::Unload,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -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"),
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ use bevy_render::{
|
|||
},
|
||||
prelude::SpatialBundle,
|
||||
primitives::Aabb,
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{Face, PrimitiveTopology},
|
||||
texture::{
|
||||
CompressedImageFormats, Image, ImageAddressMode, ImageFilterMode, ImageLoaderSettings,
|
||||
|
@ -120,7 +121,7 @@ pub struct GltfLoader {
|
|||
/// |s: &mut GltfLoaderSettings| {
|
||||
/// s.load_cameras = false;
|
||||
/// }
|
||||
/// );
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GltfLoaderSettings {
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
|
@ -830,6 +831,7 @@ pub fn extract_materials<M: Material>(
|
|||
changed_assets.remove(id);
|
||||
removed.push(*id);
|
||||
}
|
||||
AssetEvent::Unused { .. } => {}
|
||||
AssetEvent::LoadedWithDependencies { .. } => {
|
||||
// TODO: handle this
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ pub use wgpu::PrimitiveTopology;
|
|||
use crate::{
|
||||
prelude::Image,
|
||||
primitives::Aabb,
|
||||
render_asset::{PrepareAssetError, RenderAsset, RenderAssets},
|
||||
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPersistencePolicy, RenderAssets},
|
||||
render_resource::{Buffer, TextureView, VertexBufferLayout},
|
||||
renderer::RenderDevice,
|
||||
};
|
||||
|
@ -48,9 +48,10 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10;
|
|||
/// ```
|
||||
/// # use bevy_render::mesh::{Mesh, Indices};
|
||||
/// # use bevy_render::render_resource::PrimitiveTopology;
|
||||
/// # use bevy_render::render_asset::RenderAssetPersistencePolicy;
|
||||
/// fn create_simple_parallelogram() -> Mesh {
|
||||
/// // Create a new mesh using a triangle list topology, where each set of 3 vertices composes a triangle.
|
||||
/// Mesh::new(PrimitiveTopology::TriangleList)
|
||||
/// Mesh::new(PrimitiveTopology::TriangleList, RenderAssetPersistencePolicy::Unload)
|
||||
/// // Add 4 vertices, each with its own position attribute (coordinate in
|
||||
/// // 3D space), for each of the corners of the parallelogram.
|
||||
/// .with_inserted_attribute(
|
||||
|
@ -108,8 +109,6 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10;
|
|||
/// - Vertex winding order: by default, `StandardMaterial.cull_mode` is [`Some(Face::Back)`](crate::render_resource::Face),
|
||||
/// which means that Bevy would *only* render the "front" of each triangle, which
|
||||
/// is the side of the triangle from where the vertices appear in a *counter-clockwise* order.
|
||||
///
|
||||
// TODO: allow values to be unloaded after been submitting to the GPU to conserve memory
|
||||
#[derive(Asset, Debug, Clone, Reflect)]
|
||||
pub struct Mesh {
|
||||
#[reflect(ignore)]
|
||||
|
@ -123,6 +122,7 @@ pub struct Mesh {
|
|||
indices: Option<Indices>,
|
||||
morph_targets: Option<Handle<Image>>,
|
||||
morph_target_names: Option<Vec<String>>,
|
||||
pub cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
|
@ -183,13 +183,17 @@ impl Mesh {
|
|||
/// Construct a new mesh. You need to provide a [`PrimitiveTopology`] so that the
|
||||
/// renderer knows how to treat the vertex data. Most of the time this will be
|
||||
/// [`PrimitiveTopology::TriangleList`].
|
||||
pub fn new(primitive_topology: PrimitiveTopology) -> Self {
|
||||
pub fn new(
|
||||
primitive_topology: PrimitiveTopology,
|
||||
cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
) -> Self {
|
||||
Mesh {
|
||||
primitive_topology,
|
||||
attributes: Default::default(),
|
||||
indices: None,
|
||||
morph_targets: None,
|
||||
morph_target_names: None,
|
||||
cpu_persistent_access,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1057,50 +1061,48 @@ pub enum GpuBufferInfo {
|
|||
}
|
||||
|
||||
impl RenderAsset for Mesh {
|
||||
type ExtractedAsset = Mesh;
|
||||
type PreparedAsset = GpuMesh;
|
||||
type Param = (SRes<RenderDevice>, SRes<RenderAssets<Image>>);
|
||||
|
||||
/// Clones the mesh.
|
||||
fn extract_asset(&self) -> Self::ExtractedAsset {
|
||||
self.clone()
|
||||
fn persistence_policy(&self) -> RenderAssetPersistencePolicy {
|
||||
self.cpu_persistent_access
|
||||
}
|
||||
|
||||
/// Converts the extracted mesh a into [`GpuMesh`].
|
||||
fn prepare_asset(
|
||||
mesh: Self::ExtractedAsset,
|
||||
self,
|
||||
(render_device, images): &mut SystemParamItem<Self::Param>,
|
||||
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
|
||||
let vertex_buffer_data = mesh.get_vertex_buffer_data();
|
||||
) -> Result<Self::PreparedAsset, PrepareAssetError<Self>> {
|
||||
let vertex_buffer_data = self.get_vertex_buffer_data();
|
||||
let vertex_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
|
||||
usage: BufferUsages::VERTEX,
|
||||
label: Some("Mesh Vertex Buffer"),
|
||||
contents: &vertex_buffer_data,
|
||||
});
|
||||
|
||||
let buffer_info = if let Some(data) = mesh.get_index_buffer_bytes() {
|
||||
let buffer_info = if let Some(data) = self.get_index_buffer_bytes() {
|
||||
GpuBufferInfo::Indexed {
|
||||
buffer: render_device.create_buffer_with_data(&BufferInitDescriptor {
|
||||
usage: BufferUsages::INDEX,
|
||||
contents: data,
|
||||
label: Some("Mesh Index Buffer"),
|
||||
}),
|
||||
count: mesh.indices().unwrap().len() as u32,
|
||||
index_format: mesh.indices().unwrap().into(),
|
||||
count: self.indices().unwrap().len() as u32,
|
||||
index_format: self.indices().unwrap().into(),
|
||||
}
|
||||
} else {
|
||||
GpuBufferInfo::NonIndexed
|
||||
};
|
||||
|
||||
let mesh_vertex_buffer_layout = mesh.get_mesh_vertex_buffer_layout();
|
||||
let mesh_vertex_buffer_layout = self.get_mesh_vertex_buffer_layout();
|
||||
|
||||
Ok(GpuMesh {
|
||||
vertex_buffer,
|
||||
vertex_count: mesh.count_vertices() as u32,
|
||||
vertex_count: self.count_vertices() as u32,
|
||||
buffer_info,
|
||||
primitive_topology: mesh.primitive_topology(),
|
||||
primitive_topology: self.primitive_topology(),
|
||||
layout: mesh_vertex_buffer_layout,
|
||||
morph_targets: mesh
|
||||
morph_targets: self
|
||||
.morph_targets
|
||||
.and_then(|mt| images.get(&mt).map(|i| i.texture_view.clone())),
|
||||
})
|
||||
|
@ -1231,12 +1233,16 @@ fn generate_tangents_for_mesh(mesh: &Mesh) -> Result<Vec<[f32; 4]>, GenerateTang
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Mesh;
|
||||
use crate::render_asset::RenderAssetPersistencePolicy;
|
||||
use wgpu::PrimitiveTopology;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panic_invalid_format() {
|
||||
let _mesh = Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0, 0.0]]);
|
||||
let _mesh = Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0, 0.0]]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
mesh::Mesh,
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
texture::Image,
|
||||
};
|
||||
|
@ -67,6 +68,7 @@ impl MorphTargetImage {
|
|||
pub fn new(
|
||||
targets: impl ExactSizeIterator<Item = impl Iterator<Item = MorphAttributes>>,
|
||||
vertex_count: usize,
|
||||
cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
) -> Result<Self, MorphBuildError> {
|
||||
let max = MAX_TEXTURE_WIDTH;
|
||||
let target_count = targets.len();
|
||||
|
@ -101,7 +103,13 @@ impl MorphTargetImage {
|
|||
height,
|
||||
depth_or_array_layers: target_count as u32,
|
||||
};
|
||||
let image = Image::new(extents, TextureDimension::D3, data, TextureFormat::R32Float);
|
||||
let image = Image::new(
|
||||
extents,
|
||||
TextureDimension::D3,
|
||||
data,
|
||||
TextureFormat::R32Float,
|
||||
cpu_persistent_access,
|
||||
);
|
||||
Ok(MorphTargetImage(image))
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +122,7 @@ impl MorphTargetImage {
|
|||
/// This exists because Bevy's [`Mesh`] corresponds to a _single_ surface / material, whereas morph targets
|
||||
/// as defined in the GLTF spec exist on "multi-primitive meshes" (where each primitive is its own surface with its own material).
|
||||
/// Therefore in Bevy [`MorphWeights`] an a parent entity are the "canonical weights" from a GLTF perspective, which then
|
||||
/// synchronized to child [`Handle<Mesh>`] / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective).
|
||||
/// synchronized to child [`Handle<Mesh>`] / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective).
|
||||
///
|
||||
/// Add this to the parent of one or more [`Entities`](`Entity`) with a [`Handle<Mesh>`] with a [`MeshMorphWeights`].
|
||||
///
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::mesh::{Indices, Mesh};
|
||||
use crate::{
|
||||
mesh::{Indices, Mesh},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
};
|
||||
use bevy_math::{Vec2, Vec3};
|
||||
use wgpu::PrimitiveTopology;
|
||||
|
||||
|
@ -364,10 +367,13 @@ impl From<Capsule> for Mesh {
|
|||
assert_eq!(vs.len(), vert_len);
|
||||
assert_eq!(tris.len(), fs_len);
|
||||
|
||||
Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vs)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vns)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vts)
|
||||
.with_indices(Some(Indices::U32(tris)))
|
||||
Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vs)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vns)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vts)
|
||||
.with_indices(Some(Indices::U32(tris)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::mesh::{Indices, Mesh};
|
||||
use crate::{
|
||||
mesh::{Indices, Mesh},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
};
|
||||
use wgpu::PrimitiveTopology;
|
||||
|
||||
/// A cylinder which stands on the XZ plane
|
||||
|
@ -118,10 +121,13 @@ impl From<Cylinder> for Mesh {
|
|||
build_cap(true);
|
||||
build_cap(false);
|
||||
|
||||
Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::mesh::{Indices, Mesh};
|
||||
use crate::{
|
||||
mesh::{Indices, Mesh},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
};
|
||||
use hexasphere::shapes::IcoSphere;
|
||||
use thiserror::Error;
|
||||
use wgpu::PrimitiveTopology;
|
||||
|
@ -103,10 +106,13 @@ impl TryFrom<Icosphere> for Mesh {
|
|||
|
||||
let indices = Indices::U32(indices);
|
||||
|
||||
Ok(Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_indices(Some(indices))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, points)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs))
|
||||
Ok(Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_indices(Some(indices))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, points)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use crate::render_asset::RenderAssetPersistencePolicy;
|
||||
|
||||
use super::{Indices, Mesh};
|
||||
use bevy_math::*;
|
||||
|
||||
|
@ -120,11 +122,14 @@ impl From<Box> for Mesh {
|
|||
20, 21, 22, 22, 23, 20, // bottom
|
||||
]);
|
||||
|
||||
Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
.with_indices(Some(indices))
|
||||
Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
.with_indices(Some(indices))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,11 +177,14 @@ impl From<Quad> for Mesh {
|
|||
let normals: Vec<_> = vertices.iter().map(|(_, n, _)| *n).collect();
|
||||
let uvs: Vec<_> = vertices.iter().map(|(_, _, uv)| *uv).collect();
|
||||
|
||||
Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_indices(Some(indices))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_indices(Some(indices))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,11 +261,14 @@ impl From<Plane> for Mesh {
|
|||
}
|
||||
}
|
||||
|
||||
Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::mesh::{Indices, Mesh};
|
||||
use crate::{
|
||||
mesh::{Indices, Mesh},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
};
|
||||
use wgpu::PrimitiveTopology;
|
||||
|
||||
/// A regular polygon in the `XY` plane
|
||||
|
@ -55,11 +58,14 @@ impl From<RegularPolygon> for Mesh {
|
|||
indices.extend_from_slice(&[0, i + 1, i]);
|
||||
}
|
||||
|
||||
Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::mesh::{Indices, Mesh};
|
||||
use crate::{
|
||||
mesh::{Indices, Mesh},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
};
|
||||
use bevy_math::Vec3;
|
||||
use wgpu::PrimitiveTopology;
|
||||
|
||||
|
@ -84,10 +87,13 @@ impl From<Torus> for Mesh {
|
|||
}
|
||||
}
|
||||
|
||||
Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use wgpu::PrimitiveTopology;
|
||||
|
||||
use crate::mesh::{Indices, Mesh};
|
||||
use crate::{
|
||||
mesh::{Indices, Mesh},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// A sphere made of sectors and stacks.
|
||||
|
@ -80,10 +83,13 @@ impl From<UVSphere> for Mesh {
|
|||
}
|
||||
}
|
||||
|
||||
Mesh::new(PrimitiveTopology::TriangleList)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
.with_indices(Some(Indices::U32(indices)))
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use crate::{Extract, ExtractSchedule, Render, RenderApp, RenderSet};
|
||||
use crate::{ExtractSchedule, MainWorld, Render, RenderApp, RenderSet};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{Asset, AssetEvent, AssetId, Assets};
|
||||
use bevy_ecs::{
|
||||
prelude::*,
|
||||
prelude::{Commands, EventReader, IntoSystemConfigs, ResMut, Resource},
|
||||
schedule::SystemConfigs,
|
||||
system::{StaticSystemParam, SystemParam, SystemParamItem},
|
||||
system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState},
|
||||
};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_utils::{thiserror::Error, HashMap, HashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
@ -19,27 +21,43 @@ pub enum PrepareAssetError<E: Send + Sync + 'static> {
|
|||
///
|
||||
/// In the [`ExtractSchedule`] step the asset is transferred
|
||||
/// from the "main world" into the "render world".
|
||||
/// Therefore it is converted into a [`RenderAsset::ExtractedAsset`], which may be the same type
|
||||
/// as the render asset itself.
|
||||
///
|
||||
/// After that in the [`RenderSet::PrepareAssets`] step the extracted asset
|
||||
/// is transformed into its GPU-representation of type [`RenderAsset::PreparedAsset`].
|
||||
pub trait RenderAsset: Asset {
|
||||
/// The representation of the asset in the "render world".
|
||||
type ExtractedAsset: Send + Sync + 'static;
|
||||
pub trait RenderAsset: Asset + Clone {
|
||||
/// The GPU-representation of the asset.
|
||||
type PreparedAsset: Send + Sync + 'static;
|
||||
|
||||
/// Specifies all ECS data required by [`RenderAsset::prepare_asset`].
|
||||
///
|
||||
/// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`].
|
||||
type Param: SystemParam;
|
||||
/// Converts the asset into a [`RenderAsset::ExtractedAsset`].
|
||||
fn extract_asset(&self) -> Self::ExtractedAsset;
|
||||
/// Prepares the `extracted asset` for the GPU by transforming it into
|
||||
/// a [`RenderAsset::PreparedAsset`]. Therefore ECS data may be accessed via the `param`.
|
||||
|
||||
/// Whether or not to unload the asset after extracting it to the render world.
|
||||
fn persistence_policy(&self) -> RenderAssetPersistencePolicy;
|
||||
|
||||
/// Prepares the asset for the GPU by transforming it into a [`RenderAsset::PreparedAsset`].
|
||||
///
|
||||
/// ECS data may be accessed via `param`.
|
||||
fn prepare_asset(
|
||||
extracted_asset: Self::ExtractedAsset,
|
||||
self,
|
||||
param: &mut SystemParamItem<Self::Param>,
|
||||
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>>;
|
||||
) -> Result<Self::PreparedAsset, PrepareAssetError<Self>>;
|
||||
}
|
||||
|
||||
/// Whether or not to unload the [`RenderAsset`] after extracting it to the render world.
|
||||
///
|
||||
/// Unloading the asset saves on memory, as for most cases it is no longer necessary to keep
|
||||
/// it in RAM once it's been uploaded to the GPU's VRAM. However, this means you can no longer
|
||||
/// access the asset from the CPU (via the `Assets<T>` resource) once unloaded (without re-loading it).
|
||||
///
|
||||
/// If you never need access to the asset from the CPU past the first frame it's loaded on,
|
||||
/// or only need very infrequent access, then set this to Unload. Otherwise, set this to Keep.
|
||||
#[derive(Reflect, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Debug)]
|
||||
pub enum RenderAssetPersistencePolicy {
|
||||
#[default]
|
||||
Unload,
|
||||
Keep,
|
||||
}
|
||||
|
||||
/// This plugin extracts the changed assets from the "app world" into the "render world"
|
||||
|
@ -104,7 +122,7 @@ impl<A: RenderAsset> RenderAssetDependency for A {
|
|||
/// Temporarily stores the extracted and removed assets of the current frame.
|
||||
#[derive(Resource)]
|
||||
pub struct ExtractedAssets<A: RenderAsset> {
|
||||
extracted: Vec<(AssetId<A>, A::ExtractedAsset)>,
|
||||
extracted: Vec<(AssetId<A>, A)>,
|
||||
removed: Vec<AssetId<A>>,
|
||||
}
|
||||
|
||||
|
@ -160,19 +178,21 @@ impl<A: RenderAsset> RenderAssets<A> {
|
|||
|
||||
/// This system extracts all created or modified assets of the corresponding [`RenderAsset`] type
|
||||
/// into the "render world".
|
||||
fn extract_render_asset<A: RenderAsset>(
|
||||
mut commands: Commands,
|
||||
mut events: Extract<EventReader<AssetEvent<A>>>,
|
||||
assets: Extract<Res<Assets<A>>>,
|
||||
) {
|
||||
fn extract_render_asset<A: RenderAsset>(mut commands: Commands, mut main_world: ResMut<MainWorld>) {
|
||||
let mut system_state: SystemState<(EventReader<AssetEvent<A>>, ResMut<Assets<A>>)> =
|
||||
SystemState::new(&mut main_world);
|
||||
let (mut events, mut assets) = system_state.get_mut(&mut main_world);
|
||||
|
||||
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);
|
||||
}
|
||||
AssetEvent::Removed { id } => {
|
||||
AssetEvent::Removed { .. } => {}
|
||||
AssetEvent::Unused { id } => {
|
||||
changed_assets.remove(id);
|
||||
removed.push(*id);
|
||||
}
|
||||
|
@ -185,7 +205,13 @@ fn extract_render_asset<A: RenderAsset>(
|
|||
let mut extracted_assets = Vec::new();
|
||||
for id in changed_assets.drain() {
|
||||
if let Some(asset) = assets.get(id) {
|
||||
extracted_assets.push((id, asset.extract_asset()));
|
||||
if asset.persistence_policy() == RenderAssetPersistencePolicy::Unload {
|
||||
if let Some(asset) = assets.remove(id) {
|
||||
extracted_assets.push((id, asset));
|
||||
}
|
||||
} else {
|
||||
extracted_assets.push((id, asset.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,7 +225,7 @@ fn extract_render_asset<A: RenderAsset>(
|
|||
/// All assets that should be prepared next frame.
|
||||
#[derive(Resource)]
|
||||
pub struct PrepareNextFrameAssets<A: RenderAsset> {
|
||||
assets: Vec<(AssetId<A>, A::ExtractedAsset)>,
|
||||
assets: Vec<(AssetId<A>, A)>,
|
||||
}
|
||||
|
||||
impl<A: RenderAsset> Default for PrepareNextFrameAssets<A> {
|
||||
|
@ -212,16 +238,16 @@ impl<A: RenderAsset> Default for PrepareNextFrameAssets<A> {
|
|||
|
||||
/// This system prepares all assets of the corresponding [`RenderAsset`] type
|
||||
/// which where extracted this frame for the GPU.
|
||||
pub fn prepare_assets<R: RenderAsset>(
|
||||
mut extracted_assets: ResMut<ExtractedAssets<R>>,
|
||||
mut render_assets: ResMut<RenderAssets<R>>,
|
||||
mut prepare_next_frame: ResMut<PrepareNextFrameAssets<R>>,
|
||||
param: StaticSystemParam<<R as RenderAsset>::Param>,
|
||||
pub fn prepare_assets<A: RenderAsset>(
|
||||
mut extracted_assets: ResMut<ExtractedAssets<A>>,
|
||||
mut render_assets: ResMut<RenderAssets<A>>,
|
||||
mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,
|
||||
param: StaticSystemParam<<A as RenderAsset>::Param>,
|
||||
) {
|
||||
let mut param = param.into_inner();
|
||||
let queued_assets = std::mem::take(&mut prepare_next_frame.assets);
|
||||
for (id, extracted_asset) in queued_assets {
|
||||
match R::prepare_asset(extracted_asset, &mut param) {
|
||||
match extracted_asset.prepare_asset(&mut param) {
|
||||
Ok(prepared_asset) => {
|
||||
render_assets.insert(id, prepared_asset);
|
||||
}
|
||||
|
@ -231,12 +257,12 @@ pub fn prepare_assets<R: RenderAsset>(
|
|||
}
|
||||
}
|
||||
|
||||
for removed in std::mem::take(&mut extracted_assets.removed) {
|
||||
for removed in extracted_assets.removed.drain(..) {
|
||||
render_assets.remove(removed);
|
||||
}
|
||||
|
||||
for (id, extracted_asset) in std::mem::take(&mut extracted_assets.extracted) {
|
||||
match R::prepare_asset(extracted_asset, &mut param) {
|
||||
for (id, extracted_asset) in extracted_assets.extracted.drain(..) {
|
||||
match extracted_asset.prepare_asset(&mut param) {
|
||||
Ok(prepared_asset) => {
|
||||
render_assets.insert(id, prepared_asset);
|
||||
}
|
||||
|
|
|
@ -848,6 +848,7 @@ impl PipelineCache {
|
|||
mut events: Extract<EventReader<AssetEvent<Shader>>>,
|
||||
) {
|
||||
for event in events.read() {
|
||||
#[allow(clippy::match_same_arms)]
|
||||
match event {
|
||||
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
|
||||
if let Some(shader) = shaders.get(*id) {
|
||||
|
@ -855,6 +856,7 @@ impl PipelineCache {
|
|||
}
|
||||
}
|
||||
AssetEvent::Removed { id } => cache.remove_shader(*id),
|
||||
AssetEvent::Unused { .. } => {}
|
||||
AssetEvent::LoadedWithDependencies { .. } => {
|
||||
// TODO: handle this
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ impl AssetSaver for CompressedImageSaver {
|
|||
format: ImageFormatSetting::Format(ImageFormat::Basis),
|
||||
is_srgb,
|
||||
sampler: image.sampler.clone(),
|
||||
cpu_persistent_access: image.cpu_persistent_access,
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
use crate::texture::{Image, TextureFormatPixelInfo};
|
||||
use crate::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
texture::{Image, TextureFormatPixelInfo},
|
||||
};
|
||||
use bevy_asset::{
|
||||
io::{AsyncReadExt, Reader},
|
||||
AssetLoader, LoadContext,
|
||||
};
|
||||
use bevy_utils::BoxedFuture;
|
||||
use image::ImageDecoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
||||
|
||||
|
@ -12,6 +16,11 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
|||
#[derive(Clone, Default)]
|
||||
pub struct ExrTextureLoader;
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug)]
|
||||
pub struct ExrTextureLoaderSettings {
|
||||
pub cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
}
|
||||
|
||||
/// Possible errors that can be produced by [`ExrTextureLoader`]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Error)]
|
||||
|
@ -24,13 +33,13 @@ pub enum ExrTextureLoaderError {
|
|||
|
||||
impl AssetLoader for ExrTextureLoader {
|
||||
type Asset = Image;
|
||||
type Settings = ();
|
||||
type Settings = ExrTextureLoaderSettings;
|
||||
type Error = ExrTextureLoaderError;
|
||||
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
reader: &'a mut Reader,
|
||||
_settings: &'a Self::Settings,
|
||||
settings: &'a Self::Settings,
|
||||
_load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<Image, Self::Error>> {
|
||||
Box::pin(async move {
|
||||
|
@ -63,6 +72,7 @@ impl AssetLoader for ExrTextureLoader {
|
|||
TextureDimension::D2,
|
||||
buf,
|
||||
format,
|
||||
settings.cpu_persistent_access,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use crate::{render_resource::*, texture::DefaultImageSampler};
|
||||
use crate::{
|
||||
render_asset::RenderAssetPersistencePolicy, render_resource::*, texture::DefaultImageSampler,
|
||||
};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
prelude::{FromWorld, Res, ResMut},
|
||||
|
@ -76,7 +78,13 @@ fn fallback_image_new(
|
|||
let image_dimension = dimension.compatible_texture_dimension();
|
||||
let mut image = if create_texture_with_data {
|
||||
let data = vec![value; format.pixel_size()];
|
||||
Image::new_fill(extents, image_dimension, &data, format)
|
||||
Image::new_fill(
|
||||
extents,
|
||||
image_dimension,
|
||||
&data,
|
||||
format,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
} else {
|
||||
let mut image = Image::default();
|
||||
image.texture_descriptor.dimension = TextureDimension::D2;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use crate::texture::{Image, TextureFormatPixelInfo};
|
||||
use crate::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
texture::{Image, TextureFormatPixelInfo},
|
||||
};
|
||||
use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
||||
|
||||
|
@ -7,6 +11,11 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
|||
#[derive(Clone, Default)]
|
||||
pub struct HdrTextureLoader;
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug)]
|
||||
pub struct HdrTextureLoaderSettings {
|
||||
pub cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HdrTextureLoaderError {
|
||||
|
@ -18,12 +27,12 @@ pub enum HdrTextureLoaderError {
|
|||
|
||||
impl AssetLoader for HdrTextureLoader {
|
||||
type Asset = Image;
|
||||
type Settings = ();
|
||||
type Settings = HdrTextureLoaderSettings;
|
||||
type Error = HdrTextureLoaderError;
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
reader: &'a mut Reader,
|
||||
_settings: &'a (),
|
||||
settings: &'a Self::Settings,
|
||||
_load_context: &'a mut LoadContext,
|
||||
) -> bevy_utils::BoxedFuture<'a, Result<Image, Self::Error>> {
|
||||
Box::pin(async move {
|
||||
|
@ -59,6 +68,7 @@ impl AssetLoader for HdrTextureLoader {
|
|||
TextureDimension::D2,
|
||||
rgba_data,
|
||||
format,
|
||||
settings.cpu_persistent_access,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use super::dds::*;
|
|||
use super::ktx2::*;
|
||||
|
||||
use crate::{
|
||||
render_asset::{PrepareAssetError, RenderAsset},
|
||||
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPersistencePolicy},
|
||||
render_resource::{Sampler, Texture, TextureView},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
texture::BevyDefault,
|
||||
|
@ -110,6 +110,7 @@ pub struct Image {
|
|||
/// The [`ImageSampler`] to use during rendering.
|
||||
pub sampler: ImageSampler,
|
||||
pub texture_view_descriptor: Option<TextureViewDescriptor<'static>>,
|
||||
pub cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
}
|
||||
|
||||
/// Used in [`Image`], this determines what image sampler to use when rendering. The default setting,
|
||||
|
@ -466,6 +467,7 @@ impl Default for Image {
|
|||
},
|
||||
sampler: ImageSampler::Default,
|
||||
texture_view_descriptor: None,
|
||||
cpu_persistent_access: RenderAssetPersistencePolicy::Unload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -481,6 +483,7 @@ impl Image {
|
|||
dimension: TextureDimension,
|
||||
data: Vec<u8>,
|
||||
format: TextureFormat,
|
||||
cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
) -> Self {
|
||||
debug_assert_eq!(
|
||||
size.volume() * format.pixel_size(),
|
||||
|
@ -494,6 +497,7 @@ impl Image {
|
|||
image.texture_descriptor.dimension = dimension;
|
||||
image.texture_descriptor.size = size;
|
||||
image.texture_descriptor.format = format;
|
||||
image.cpu_persistent_access = cpu_persistent_access;
|
||||
image
|
||||
}
|
||||
|
||||
|
@ -507,10 +511,12 @@ impl Image {
|
|||
dimension: TextureDimension,
|
||||
pixel: &[u8],
|
||||
format: TextureFormat,
|
||||
cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
) -> Self {
|
||||
let mut value = Image::default();
|
||||
value.texture_descriptor.format = format;
|
||||
value.texture_descriptor.dimension = dimension;
|
||||
value.cpu_persistent_access = cpu_persistent_access;
|
||||
value.resize(size);
|
||||
|
||||
debug_assert_eq!(
|
||||
|
@ -631,7 +637,9 @@ impl Image {
|
|||
}
|
||||
_ => None,
|
||||
})
|
||||
.map(|(dyn_img, is_srgb)| Self::from_dynamic(dyn_img, is_srgb))
|
||||
.map(|(dyn_img, is_srgb)| {
|
||||
Self::from_dynamic(dyn_img, is_srgb, self.cpu_persistent_access)
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a bytes buffer in a [`Image`], according to type `image_type`, using the `image`
|
||||
|
@ -642,6 +650,7 @@ impl Image {
|
|||
#[allow(unused_variables)] supported_compressed_formats: CompressedImageFormats,
|
||||
is_srgb: bool,
|
||||
image_sampler: ImageSampler,
|
||||
cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
) -> Result<Image, TextureError> {
|
||||
let format = image_type.to_image_format()?;
|
||||
|
||||
|
@ -670,7 +679,7 @@ impl Image {
|
|||
reader.set_format(image_crate_format);
|
||||
reader.no_limits();
|
||||
let dyn_img = reader.decode()?;
|
||||
Self::from_dynamic(dyn_img, is_srgb)
|
||||
Self::from_dynamic(dyn_img, is_srgb, cpu_persistent_access)
|
||||
}
|
||||
};
|
||||
image.sampler = image_sampler;
|
||||
|
@ -803,7 +812,6 @@ pub struct GpuImage {
|
|||
}
|
||||
|
||||
impl RenderAsset for Image {
|
||||
type ExtractedAsset = Image;
|
||||
type PreparedAsset = GpuImage;
|
||||
type Param = (
|
||||
SRes<RenderDevice>,
|
||||
|
@ -811,34 +819,32 @@ impl RenderAsset for Image {
|
|||
SRes<DefaultImageSampler>,
|
||||
);
|
||||
|
||||
/// Clones the Image.
|
||||
fn extract_asset(&self) -> Self::ExtractedAsset {
|
||||
self.clone()
|
||||
fn persistence_policy(&self) -> RenderAssetPersistencePolicy {
|
||||
self.cpu_persistent_access
|
||||
}
|
||||
|
||||
/// Converts the extracted image into a [`GpuImage`].
|
||||
fn prepare_asset(
|
||||
image: Self::ExtractedAsset,
|
||||
self,
|
||||
(render_device, render_queue, default_sampler): &mut SystemParamItem<Self::Param>,
|
||||
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
|
||||
) -> Result<Self::PreparedAsset, PrepareAssetError<Self>> {
|
||||
let texture = render_device.create_texture_with_data(
|
||||
render_queue,
|
||||
&image.texture_descriptor,
|
||||
&image.data,
|
||||
&self.texture_descriptor,
|
||||
&self.data,
|
||||
);
|
||||
|
||||
let texture_view = texture.create_view(
|
||||
image
|
||||
.texture_view_descriptor
|
||||
self.texture_view_descriptor
|
||||
.or_else(|| Some(TextureViewDescriptor::default()))
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
);
|
||||
let size = Vec2::new(
|
||||
image.texture_descriptor.size.width as f32,
|
||||
image.texture_descriptor.size.height as f32,
|
||||
self.texture_descriptor.size.width as f32,
|
||||
self.texture_descriptor.size.height as f32,
|
||||
);
|
||||
let sampler = match image.sampler {
|
||||
let sampler = match self.sampler {
|
||||
ImageSampler::Default => (***default_sampler).clone(),
|
||||
ImageSampler::Descriptor(descriptor) => {
|
||||
render_device.create_sampler(&descriptor.as_wgpu())
|
||||
|
@ -848,10 +854,10 @@ impl RenderAsset for Image {
|
|||
Ok(GpuImage {
|
||||
texture,
|
||||
texture_view,
|
||||
texture_format: image.texture_descriptor.format,
|
||||
texture_format: self.texture_descriptor.format,
|
||||
sampler,
|
||||
size,
|
||||
mip_level_count: image.texture_descriptor.mip_level_count,
|
||||
mip_level_count: self.texture_descriptor.mip_level_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -918,6 +924,7 @@ impl CompressedImageFormats {
|
|||
mod test {
|
||||
|
||||
use super::*;
|
||||
use crate::render_asset::RenderAssetPersistencePolicy;
|
||||
|
||||
#[test]
|
||||
fn image_size() {
|
||||
|
@ -931,6 +938,7 @@ mod test {
|
|||
TextureDimension::D2,
|
||||
&[0, 0, 0, 255],
|
||||
TextureFormat::Rgba8Unorm,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
);
|
||||
assert_eq!(
|
||||
Vec2::new(size.width as f32, size.height as f32),
|
||||
|
|
|
@ -3,6 +3,7 @@ use bevy_ecs::prelude::{FromWorld, World};
|
|||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
renderer::RenderDevice,
|
||||
texture::{Image, ImageFormat, ImageType, TextureError},
|
||||
};
|
||||
|
@ -57,6 +58,7 @@ pub struct ImageLoaderSettings {
|
|||
pub format: ImageFormatSetting,
|
||||
pub is_srgb: bool,
|
||||
pub sampler: ImageSampler,
|
||||
pub cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
}
|
||||
|
||||
impl Default for ImageLoaderSettings {
|
||||
|
@ -65,6 +67,7 @@ impl Default for ImageLoaderSettings {
|
|||
format: ImageFormatSetting::default(),
|
||||
is_srgb: true,
|
||||
sampler: ImageSampler::Default,
|
||||
cpu_persistent_access: RenderAssetPersistencePolicy::Unload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,6 +107,7 @@ impl AssetLoader for ImageLoader {
|
|||
self.supported_compressed_formats,
|
||||
settings.is_srgb,
|
||||
settings.sampler.clone(),
|
||||
settings.cpu_persistent_access,
|
||||
)
|
||||
.map_err(|err| FileTextureError {
|
||||
error: err,
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
use crate::texture::{Image, TextureFormatPixelInfo};
|
||||
use crate::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
texture::{Image, TextureFormatPixelInfo},
|
||||
};
|
||||
use image::{DynamicImage, ImageBuffer};
|
||||
use thiserror::Error;
|
||||
use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
||||
|
||||
impl Image {
|
||||
/// Converts a [`DynamicImage`] to an [`Image`].
|
||||
pub fn from_dynamic(dyn_img: DynamicImage, is_srgb: bool) -> Image {
|
||||
pub fn from_dynamic(
|
||||
dyn_img: DynamicImage,
|
||||
is_srgb: bool,
|
||||
cpu_persistent_access: RenderAssetPersistencePolicy,
|
||||
) -> Image {
|
||||
use bevy_core::cast_slice;
|
||||
let width;
|
||||
let height;
|
||||
|
@ -151,6 +158,7 @@ impl Image {
|
|||
TextureDimension::D2,
|
||||
data,
|
||||
format,
|
||||
cpu_persistent_access,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -214,6 +222,7 @@ mod test {
|
|||
use image::{GenericImage, Rgba};
|
||||
|
||||
use super::*;
|
||||
use crate::render_asset::RenderAssetPersistencePolicy;
|
||||
|
||||
#[test]
|
||||
fn two_way_conversion() {
|
||||
|
@ -221,7 +230,8 @@ mod test {
|
|||
let mut initial = DynamicImage::new_rgba8(1, 1);
|
||||
initial.put_pixel(0, 0, Rgba::from([132, 3, 7, 200]));
|
||||
|
||||
let image = Image::from_dynamic(initial.clone(), true);
|
||||
let image =
|
||||
Image::from_dynamic(initial.clone(), true, RenderAssetPersistencePolicy::Unload);
|
||||
|
||||
// NOTE: Fails if `is_srbg = false` or the dynamic image is of the type rgb8.
|
||||
assert_eq!(initial, image.try_into_dynamic().unwrap());
|
||||
|
|
|
@ -14,6 +14,7 @@ use wgpu::{
|
|||
|
||||
use crate::{
|
||||
prelude::{Image, Shader},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{
|
||||
binding_types::texture_2d, BindGroup, BindGroupLayout, BindGroupLayoutEntries, Buffer,
|
||||
CachedRenderPipelineId, FragmentState, PipelineCache, RenderPipelineDescriptor,
|
||||
|
@ -363,6 +364,7 @@ pub(crate) fn collect_screenshots(world: &mut World) {
|
|||
wgpu::TextureDimension::D2,
|
||||
result,
|
||||
texture_format,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
));
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use crate::TextureAtlas;
|
||||
use bevy_asset::Assets;
|
||||
use bevy_math::{IVec2, Rect, Vec2};
|
||||
use bevy_render::texture::{Image, TextureFormatPixelInfo};
|
||||
use bevy_render::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
texture::{Image, TextureFormatPixelInfo},
|
||||
};
|
||||
use guillotiere::{size2, Allocation, AtlasAllocator};
|
||||
|
||||
/// Helper utility to update [`TextureAtlas`] on the fly.
|
||||
|
@ -28,7 +31,9 @@ impl DynamicTextureAtlasBuilder {
|
|||
}
|
||||
|
||||
/// Add a new texture to [`TextureAtlas`].
|
||||
/// It is user's responsibility to pass in the correct [`TextureAtlas`]
|
||||
/// It is user's responsibility to pass in the correct [`TextureAtlas`],
|
||||
/// and that [`TextureAtlas::texture`] has [`Image::cpu_persistent_access`]
|
||||
/// set to [`RenderAssetPersistencePolicy::Keep`]
|
||||
pub fn add_texture(
|
||||
&mut self,
|
||||
texture_atlas: &mut TextureAtlas,
|
||||
|
@ -41,6 +46,11 @@ impl DynamicTextureAtlasBuilder {
|
|||
));
|
||||
if let Some(allocation) = allocation {
|
||||
let atlas_texture = textures.get_mut(&texture_atlas.texture).unwrap();
|
||||
assert_eq!(
|
||||
atlas_texture.cpu_persistent_access,
|
||||
RenderAssetPersistencePolicy::Keep
|
||||
);
|
||||
|
||||
self.place_texture(atlas_texture, allocation, texture);
|
||||
let mut rect: Rect = to_rect(allocation.rectangle);
|
||||
rect.max -= self.padding as f32;
|
||||
|
|
|
@ -515,6 +515,7 @@ pub fn extract_materials_2d<M: Material2d>(
|
|||
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);
|
||||
|
@ -523,7 +524,7 @@ pub fn extract_materials_2d<M: Material2d>(
|
|||
changed_assets.remove(id);
|
||||
removed.push(*id);
|
||||
}
|
||||
|
||||
AssetEvent::Unused { .. } => {}
|
||||
AssetEvent::LoadedWithDependencies { .. } => {
|
||||
// TODO: handle this
|
||||
}
|
||||
|
|
|
@ -588,8 +588,9 @@ pub fn prepare_sprites(
|
|||
// If an image has changed, the GpuImage has (probably) changed
|
||||
for event in &events.images {
|
||||
match event {
|
||||
AssetEvent::Added {..} |
|
||||
// images don't have dependencies
|
||||
AssetEvent::Added { .. } |
|
||||
AssetEvent::Unused { .. } |
|
||||
// Images don't have dependencies
|
||||
AssetEvent::LoadedWithDependencies { .. } => {}
|
||||
AssetEvent::Modified { id } | AssetEvent::Removed { id } => {
|
||||
image_bind_groups.values.remove(id);
|
||||
|
|
|
@ -2,6 +2,7 @@ use bevy_asset::{AssetId, Assets};
|
|||
use bevy_log::{debug, error, warn};
|
||||
use bevy_math::{Rect, UVec2, Vec2};
|
||||
use bevy_render::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
texture::{Image, TextureFormatPixelInfo},
|
||||
};
|
||||
|
@ -208,6 +209,7 @@ impl TextureAtlasBuilder {
|
|||
self.format.pixel_size() * (current_width * current_height) as usize
|
||||
],
|
||||
self.format,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
);
|
||||
Some(rect_placements)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph};
|
|||
use bevy_asset::Asset;
|
||||
use bevy_reflect::TypePath;
|
||||
use bevy_render::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
texture::Image,
|
||||
};
|
||||
|
@ -44,6 +45,7 @@ impl Font {
|
|||
.flat_map(|a| vec![255, 255, 255, (*a * 255.0) as u8])
|
||||
.collect::<Vec<u8>>(),
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ use ab_glyph::{GlyphId, Point};
|
|||
use bevy_asset::{Assets, Handle};
|
||||
use bevy_math::Vec2;
|
||||
use bevy_render::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
texture::Image,
|
||||
};
|
||||
|
@ -60,6 +61,8 @@ impl FontAtlas {
|
|||
TextureDimension::D2,
|
||||
&[0, 0, 0, 0],
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
// Need to keep this image CPU persistent in order to add additional glyphs later on
|
||||
RenderAssetPersistencePolicy::Keep,
|
||||
));
|
||||
let texture_atlas = TextureAtlas::new_empty(atlas_texture, size);
|
||||
Self {
|
||||
|
|
|
@ -789,6 +789,7 @@ pub fn prepare_uinodes(
|
|||
for event in &events.images {
|
||||
match event {
|
||||
AssetEvent::Added { .. } |
|
||||
AssetEvent::Unused { .. } |
|
||||
// Images don't have dependencies
|
||||
AssetEvent::LoadedWithDependencies { .. } => {}
|
||||
AssetEvent::Modified { id } | AssetEvent::Removed { id } => {
|
||||
|
|
|
@ -615,6 +615,7 @@ pub fn extract_ui_materials<M: UiMaterial>(
|
|||
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);
|
||||
|
@ -623,8 +624,9 @@ pub fn extract_ui_materials<M: UiMaterial>(
|
|||
changed_assets.remove(id);
|
||||
removed.push(*id);
|
||||
}
|
||||
AssetEvent::Unused { .. } => {}
|
||||
AssetEvent::LoadedWithDependencies { .. } => {
|
||||
// not implemented
|
||||
// TODO: handle this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -216,7 +216,7 @@ fn queue_text(
|
|||
/// ## World Resources
|
||||
///
|
||||
/// [`ResMut<Assets<Image>>`](Assets<Image>) -- This system only adds new [`Image`] assets.
|
||||
/// It does not modify or observe existing ones.
|
||||
/// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn text_system(
|
||||
mut textures: ResMut<Assets<Image>>,
|
||||
|
|
|
@ -10,6 +10,7 @@ use bevy::{
|
|||
prelude::*,
|
||||
render::{
|
||||
mesh::{Indices, MeshVertexAttribute},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_asset::RenderAssets,
|
||||
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
|
||||
render_resource::{
|
||||
|
@ -47,8 +48,13 @@ fn star(
|
|||
// We will specify here what kind of topology is used to define the mesh,
|
||||
// that is, how triangles are built from the vertices. We will use a
|
||||
// triangle list, meaning that each vertex of the triangle has to be
|
||||
// specified.
|
||||
let mut star = Mesh::new(PrimitiveTopology::TriangleList);
|
||||
// specified. We set `cpu_persistent_access` to unload, meaning this mesh
|
||||
// will not be accessible in future frames from the `meshes` resource, in
|
||||
// order to save on memory once it has been uploaded to the GPU.
|
||||
let mut star = Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
);
|
||||
|
||||
// Vertices need to have a position attribute. We will use the following
|
||||
// vertices (I hope you can spot the star in the schema).
|
||||
|
|
|
@ -5,7 +5,10 @@ use std::f32::consts::PI;
|
|||
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
render::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
|
@ -117,5 +120,6 @@ fn uv_debug_texture() -> Image {
|
|||
TextureDimension::D2,
|
||||
&texture_data,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ use bevy::{
|
|||
pbr::CascadeShadowConfigBuilder,
|
||||
prelude::*,
|
||||
render::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
texture::{ImageSampler, ImageSamplerDescriptor},
|
||||
},
|
||||
|
@ -378,6 +379,7 @@ fn uv_debug_texture() -> Image {
|
|||
TextureDimension::D2,
|
||||
&texture_data,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
);
|
||||
img.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor::default());
|
||||
img
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
// ! assign a custom UV mapping for a custom texture,
|
||||
// ! and how to change the UV mapping at run-time.
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::mesh::Indices;
|
||||
use bevy::render::mesh::VertexAttributeValues;
|
||||
use bevy::render::render_resource::PrimitiveTopology;
|
||||
use bevy::render::{
|
||||
mesh::{Indices, VertexAttributeValues},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::PrimitiveTopology,
|
||||
};
|
||||
|
||||
// Define a "marker" component to mark the custom mesh. Marker components are often used in Bevy for
|
||||
// filtering entities in queries with With, they're usually not queried directly since they don't contain information within them.
|
||||
|
@ -120,7 +122,8 @@ fn input_handler(
|
|||
|
||||
#[rustfmt::skip]
|
||||
fn create_cube_mesh() -> Mesh {
|
||||
Mesh::new(PrimitiveTopology::TriangleList)
|
||||
// Keep the mesh data accessible in future frames to be able to mutate it in toggle_texture.
|
||||
Mesh::new(PrimitiveTopology::TriangleList, RenderAssetPersistencePolicy::Keep)
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_POSITION,
|
||||
// Each array is an [x, y, z] coordinate in local space.
|
||||
|
|
|
@ -6,6 +6,7 @@ use bevy::{
|
|||
reflect::TypePath,
|
||||
render::{
|
||||
mesh::{MeshVertexBufferLayout, PrimitiveTopology},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{
|
||||
AsBindGroup, PolygonMode, RenderPipelineDescriptor, ShaderRef,
|
||||
SpecializedMeshPipelineError,
|
||||
|
@ -94,11 +95,14 @@ impl From<LineList> for Mesh {
|
|||
fn from(line: LineList) -> Self {
|
||||
let vertices: Vec<_> = line.lines.into_iter().flat_map(|(a, b)| [a, b]).collect();
|
||||
|
||||
// This tells wgpu that the positions are list of lines
|
||||
// where every pair is a start and end point
|
||||
Mesh::new(PrimitiveTopology::LineList)
|
||||
// Add the vertices positions as an attribute
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
|
||||
Mesh::new(
|
||||
// This tells wgpu that the positions are list of lines
|
||||
// where every pair is a start and end point
|
||||
PrimitiveTopology::LineList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
// Add the vertices positions as an attribute
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,10 +114,13 @@ pub struct LineStrip {
|
|||
|
||||
impl From<LineStrip> for Mesh {
|
||||
fn from(line: LineStrip) -> Self {
|
||||
// This tells wgpu that the positions are a list of points
|
||||
// where a line will be drawn between each consecutive point
|
||||
Mesh::new(PrimitiveTopology::LineStrip)
|
||||
// Add the point positions as an attribute
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, line.points)
|
||||
Mesh::new(
|
||||
// This tells wgpu that the positions are a list of points
|
||||
// where a line will be drawn between each consecutive point
|
||||
PrimitiveTopology::LineStrip,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
// Add the point positions as an attribute
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, line.points)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use bevy::{
|
|||
prelude::*,
|
||||
reflect::TypePath,
|
||||
render::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{AsBindGroup, Extent3d, ShaderRef, TextureDimension, TextureFormat},
|
||||
texture::{ImageSampler, ImageSamplerDescriptor},
|
||||
view::ColorGrading,
|
||||
|
@ -678,6 +679,7 @@ fn uv_debug_texture() -> Image {
|
|||
TextureDimension::D2,
|
||||
&texture_data,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
);
|
||||
img.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor::default());
|
||||
img
|
||||
|
|
|
@ -6,9 +6,12 @@ use std::f32::consts::*;
|
|||
use bevy::{
|
||||
pbr::AmbientLight,
|
||||
prelude::*,
|
||||
render::mesh::{
|
||||
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
|
||||
Indices, PrimitiveTopology, VertexAttributeValues,
|
||||
render::{
|
||||
mesh::{
|
||||
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
|
||||
Indices, PrimitiveTopology, VertexAttributeValues,
|
||||
},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
},
|
||||
};
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
|
@ -52,68 +55,71 @@ fn setup(
|
|||
]));
|
||||
|
||||
// Create a mesh
|
||||
let mesh = Mesh::new(PrimitiveTopology::TriangleList)
|
||||
// Set mesh vertex positions
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_POSITION,
|
||||
vec![
|
||||
[0.0, 0.0, 0.0],
|
||||
[1.0, 0.0, 0.0],
|
||||
[0.0, 0.5, 0.0],
|
||||
[1.0, 0.5, 0.0],
|
||||
[0.0, 1.0, 0.0],
|
||||
[1.0, 1.0, 0.0],
|
||||
[0.0, 1.5, 0.0],
|
||||
[1.0, 1.5, 0.0],
|
||||
[0.0, 2.0, 0.0],
|
||||
[1.0, 2.0, 0.0],
|
||||
],
|
||||
)
|
||||
// Set mesh vertex normals
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 10])
|
||||
// Set mesh vertex joint indices for mesh skinning.
|
||||
// Each vertex gets 4 indices used to address the `JointTransforms` array in the vertex shader
|
||||
// as well as `SkinnedMeshJoint` array in the `SkinnedMesh` component.
|
||||
// This means that a maximum of 4 joints can affect a single vertex.
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_JOINT_INDEX,
|
||||
// Need to be explicit here as [u16; 4] could be either Uint16x4 or Unorm16x4.
|
||||
VertexAttributeValues::Uint16x4(vec![
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
]),
|
||||
)
|
||||
// Set mesh vertex joint weights for mesh skinning.
|
||||
// Each vertex gets 4 joint weights corresponding to the 4 joint indices assigned to it.
|
||||
// The sum of these weights should equal to 1.
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_JOINT_WEIGHT,
|
||||
vec![
|
||||
[1.00, 0.00, 0.0, 0.0],
|
||||
[1.00, 0.00, 0.0, 0.0],
|
||||
[0.75, 0.25, 0.0, 0.0],
|
||||
[0.75, 0.25, 0.0, 0.0],
|
||||
[0.50, 0.50, 0.0, 0.0],
|
||||
[0.50, 0.50, 0.0, 0.0],
|
||||
[0.25, 0.75, 0.0, 0.0],
|
||||
[0.25, 0.75, 0.0, 0.0],
|
||||
[0.00, 1.00, 0.0, 0.0],
|
||||
[0.00, 1.00, 0.0, 0.0],
|
||||
],
|
||||
)
|
||||
// Tell bevy to construct triangles from a list of vertex indices,
|
||||
// where each 3 vertex indices form an triangle.
|
||||
.with_indices(Some(Indices::U16(vec![
|
||||
0, 1, 3, 0, 3, 2, 2, 3, 5, 2, 5, 4, 4, 5, 7, 4, 7, 6, 6, 7, 9, 6, 9, 8,
|
||||
])));
|
||||
let mesh = Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)
|
||||
// Set mesh vertex positions
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_POSITION,
|
||||
vec![
|
||||
[0.0, 0.0, 0.0],
|
||||
[1.0, 0.0, 0.0],
|
||||
[0.0, 0.5, 0.0],
|
||||
[1.0, 0.5, 0.0],
|
||||
[0.0, 1.0, 0.0],
|
||||
[1.0, 1.0, 0.0],
|
||||
[0.0, 1.5, 0.0],
|
||||
[1.0, 1.5, 0.0],
|
||||
[0.0, 2.0, 0.0],
|
||||
[1.0, 2.0, 0.0],
|
||||
],
|
||||
)
|
||||
// Set mesh vertex normals
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 10])
|
||||
// Set mesh vertex joint indices for mesh skinning.
|
||||
// Each vertex gets 4 indices used to address the `JointTransforms` array in the vertex shader
|
||||
// as well as `SkinnedMeshJoint` array in the `SkinnedMesh` component.
|
||||
// This means that a maximum of 4 joints can affect a single vertex.
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_JOINT_INDEX,
|
||||
// Need to be explicit here as [u16; 4] could be either Uint16x4 or Unorm16x4.
|
||||
VertexAttributeValues::Uint16x4(vec![
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
]),
|
||||
)
|
||||
// Set mesh vertex joint weights for mesh skinning.
|
||||
// Each vertex gets 4 joint weights corresponding to the 4 joint indices assigned to it.
|
||||
// The sum of these weights should equal to 1.
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_JOINT_WEIGHT,
|
||||
vec![
|
||||
[1.00, 0.00, 0.0, 0.0],
|
||||
[1.00, 0.00, 0.0, 0.0],
|
||||
[0.75, 0.25, 0.0, 0.0],
|
||||
[0.75, 0.25, 0.0, 0.0],
|
||||
[0.50, 0.50, 0.0, 0.0],
|
||||
[0.50, 0.50, 0.0, 0.0],
|
||||
[0.25, 0.75, 0.0, 0.0],
|
||||
[0.25, 0.75, 0.0, 0.0],
|
||||
[0.00, 1.00, 0.0, 0.0],
|
||||
[0.00, 1.00, 0.0, 0.0],
|
||||
],
|
||||
)
|
||||
// Tell bevy to construct triangles from a list of vertex indices,
|
||||
// where each 3 vertex indices form an triangle.
|
||||
.with_indices(Some(Indices::U16(vec![
|
||||
0, 1, 3, 0, 3, 2, 2, 3, 5, 2, 5, 4, 4, 5, 7, 4, 7, 6, 6, 7, 9, 6, 9, 8,
|
||||
])));
|
||||
|
||||
let mesh = meshes.add(mesh);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ use bevy::{
|
|||
prelude::*,
|
||||
render::{
|
||||
extract_resource::{ExtractResource, ExtractResourcePlugin},
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_asset::RenderAssets,
|
||||
render_graph::{self, RenderGraph},
|
||||
render_resource::{binding_types::texture_storage_2d, *},
|
||||
|
@ -48,6 +49,7 @@ fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
|
|||
TextureDimension::D2,
|
||||
&[0, 0, 0, 255],
|
||||
TextureFormat::Rgba8Unorm,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
);
|
||||
image.texture_descriptor.usage =
|
||||
TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING;
|
||||
|
|
|
@ -8,7 +8,10 @@ use argh::FromArgs;
|
|||
use bevy::{
|
||||
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
||||
prelude::*,
|
||||
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
render::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
},
|
||||
sprite::{MaterialMesh2dBundle, Mesh2dHandle},
|
||||
utils::Duration,
|
||||
window::{PresentMode, WindowResolution},
|
||||
|
@ -542,6 +545,7 @@ fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut As
|
|||
TextureDimension::D2,
|
||||
&pixel,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,10 @@ use bevy::{
|
|||
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
||||
math::{DVec2, DVec3},
|
||||
prelude::*,
|
||||
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
render::{
|
||||
render_asset::RenderAssetPersistencePolicy,
|
||||
render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
},
|
||||
window::{PresentMode, WindowPlugin, WindowResolution},
|
||||
};
|
||||
use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng};
|
||||
|
@ -198,6 +201,7 @@ fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>>
|
|||
TextureDimension::D2,
|
||||
pixel,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
RenderAssetPersistencePolicy::Unload,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
|
|
Loading…
Add table
Reference in a new issue