RenderAssetPlugin
This commit is contained in:
		
							parent
							
								
									09043b66ce
								
							
						
					
					
						commit
						3ef951dcbc
					
				| @ -2,6 +2,7 @@ pub mod camera; | ||||
| pub mod color; | ||||
| pub mod core_pipeline; | ||||
| pub mod mesh; | ||||
| pub mod render_asset; | ||||
| pub mod render_graph; | ||||
| pub mod render_phase; | ||||
| pub mod render_resource; | ||||
| @ -22,7 +23,7 @@ use crate::{ | ||||
|     texture::ImagePlugin, | ||||
|     view::{ViewPlugin, WindowRenderPlugin}, | ||||
| }; | ||||
| use bevy_app::{App, Plugin, StartupStage}; | ||||
| use bevy_app::{App, Plugin}; | ||||
| use bevy_ecs::prelude::*; | ||||
| 
 | ||||
| #[derive(Default)] | ||||
|  | ||||
							
								
								
									
										104
									
								
								pipelined/bevy_render2/src/render_asset.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								pipelined/bevy_render2/src/render_asset.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,104 @@ | ||||
| use std::marker::PhantomData; | ||||
| 
 | ||||
| use crate::{ | ||||
|     renderer::{RenderDevice, RenderQueue}, | ||||
|     RenderStage, | ||||
| }; | ||||
| use bevy_app::{App, Plugin}; | ||||
| use bevy_asset::{Asset, AssetEvent, Assets, Handle}; | ||||
| use bevy_ecs::prelude::*; | ||||
| use bevy_utils::{HashMap, HashSet}; | ||||
| 
 | ||||
| pub trait RenderAsset: Asset { | ||||
|     type ExtractedAsset: Send + Sync + 'static; | ||||
|     type PreparedAsset: Send + Sync + 'static; | ||||
|     fn extract_asset(&self) -> Self::ExtractedAsset; | ||||
|     fn prepare_asset( | ||||
|         extracted_asset: Self::ExtractedAsset, | ||||
|         render_device: &RenderDevice, | ||||
|         render_queue: &RenderQueue, | ||||
|     ) -> Self::PreparedAsset; | ||||
| } | ||||
| 
 | ||||
| /// Extracts assets into gpu-usable data
 | ||||
| #[derive(Default)] | ||||
| pub struct RenderAssetPlugin<A: RenderAsset>(PhantomData<fn() -> A>); | ||||
| 
 | ||||
| impl<A: RenderAsset> Plugin for RenderAssetPlugin<A> { | ||||
|     fn build(&self, app: &mut App) { | ||||
|         let render_app = app.sub_app_mut(0); | ||||
|         render_app | ||||
|             .init_resource::<ExtractedAssets<A>>() | ||||
|             .init_resource::<RenderAssets<A>>() | ||||
|             .add_system_to_stage(RenderStage::Extract, extract_render_asset::<A>.system()) | ||||
|             .add_system_to_stage(RenderStage::Prepare, prepare_render_asset::<A>.system()); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| struct ExtractedAssets<A: RenderAsset> { | ||||
|     extracted: Vec<(Handle<A>, A::ExtractedAsset)>, | ||||
|     removed: Vec<Handle<A>>, | ||||
| } | ||||
| 
 | ||||
| impl<A: RenderAsset> Default for ExtractedAssets<A> { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             extracted: Default::default(), | ||||
|             removed: Default::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub type RenderAssets<A: RenderAsset> = HashMap<Handle<A>, A::PreparedAsset>; | ||||
| 
 | ||||
| fn extract_render_asset<A: RenderAsset>( | ||||
|     mut commands: Commands, | ||||
|     mut events: EventReader<AssetEvent<A>>, | ||||
|     assets: Res<Assets<A>>, | ||||
| ) { | ||||
|     let mut changed_assets = HashSet::default(); | ||||
|     let mut removed = Vec::new(); | ||||
|     for event in events.iter() { | ||||
|         match event { | ||||
|             AssetEvent::Created { handle } => { | ||||
|                 changed_assets.insert(handle); | ||||
|             } | ||||
|             AssetEvent::Modified { handle } => { | ||||
|                 changed_assets.insert(handle); | ||||
|             } | ||||
|             AssetEvent::Removed { handle } => { | ||||
|                 if !changed_assets.remove(handle) { | ||||
|                     removed.push(handle.clone_weak()); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     let mut extracted_assets = Vec::new(); | ||||
|     for handle in changed_assets.drain() { | ||||
|         if let Some(asset) = assets.get(handle) { | ||||
|             extracted_assets.push((handle.clone_weak(), asset.extract_asset())); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     commands.insert_resource(ExtractedAssets { | ||||
|         extracted: extracted_assets, | ||||
|         removed, | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| fn prepare_render_asset<R: RenderAsset>( | ||||
|     mut extracted_assets: ResMut<ExtractedAssets<R>>, | ||||
|     mut render_assets: ResMut<RenderAssets<R>>, | ||||
|     render_device: Res<RenderDevice>, | ||||
|     render_queue: Res<RenderQueue>, | ||||
| ) { | ||||
|     for removed in extracted_assets.removed.iter() { | ||||
|         render_assets.remove(removed); | ||||
|     } | ||||
| 
 | ||||
|     for (handle, extracted_asset) in extracted_assets.extracted.drain(..) { | ||||
|         let prepared_asset = R::prepare_asset(extracted_asset, &render_device, &render_queue); | ||||
|         render_assets.insert(handle, prepared_asset); | ||||
|     } | ||||
| } | ||||
| @ -1,25 +1,23 @@ | ||||
| use super::image_texture_conversion::image_to_texture; | ||||
| use crate::render_resource::{Sampler, Texture, TextureView}; | ||||
| use crate::{ | ||||
|     render_asset::RenderAsset, | ||||
|     render_resource::{Sampler, Texture, TextureView}, | ||||
|     renderer::{RenderDevice, RenderQueue}, | ||||
| }; | ||||
| use bevy_reflect::TypeUuid; | ||||
| use thiserror::Error; | ||||
| use wgpu::{Extent3d, TextureDimension, TextureFormat}; | ||||
| use wgpu::{ | ||||
|     Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, TextureDimension, TextureFormat, | ||||
|     TextureViewDescriptor, | ||||
| }; | ||||
| 
 | ||||
| pub const TEXTURE_ASSET_INDEX: u64 = 0; | ||||
| pub const SAMPLER_ASSET_INDEX: u64 = 1; | ||||
| 
 | ||||
| // TODO: this shouldn't live in the Texture type
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct ImageGpuData { | ||||
|     pub texture: Texture, | ||||
|     pub texture_view: TextureView, | ||||
|     pub sampler: Sampler, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone, TypeUuid)] | ||||
| #[uuid = "6ea26da6-6cf8-4ea2-9986-1d7bf6c17d6f"] | ||||
| pub struct Image { | ||||
|     pub data: Vec<u8>, | ||||
|     pub gpu_data: Option<ImageGpuData>, | ||||
|     // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors
 | ||||
|     pub texture_descriptor: wgpu::TextureDescriptor<'static>, | ||||
|     pub sampler_descriptor: wgpu::SamplerDescriptor<'static>, | ||||
| @ -29,7 +27,6 @@ impl Default for Image { | ||||
|     fn default() -> Self { | ||||
|         Image { | ||||
|             data: Default::default(), | ||||
|             gpu_data: None, | ||||
|             texture_descriptor: wgpu::TextureDescriptor { | ||||
|                 size: wgpu::Extent3d { | ||||
|                     width: 1, | ||||
| @ -336,3 +333,57 @@ impl TextureFormatPixelInfo for TextureFormat { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct GpuImage { | ||||
|     pub texture: Texture, | ||||
|     pub texture_view: TextureView, | ||||
|     pub sampler: Sampler, | ||||
| } | ||||
| 
 | ||||
| impl RenderAsset for Image { | ||||
|     type ExtractedAsset = Image; | ||||
|     type PreparedAsset = GpuImage; | ||||
| 
 | ||||
|     fn extract_asset(&self) -> Self::ExtractedAsset { | ||||
|         self.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn prepare_asset( | ||||
|         image: Self::ExtractedAsset, | ||||
|         render_device: &RenderDevice, | ||||
|         render_queue: &RenderQueue, | ||||
|     ) -> Self::PreparedAsset { | ||||
|         let texture = render_device.create_texture(&image.texture_descriptor); | ||||
|         let sampler = render_device.create_sampler(&image.sampler_descriptor); | ||||
| 
 | ||||
|         let width = image.texture_descriptor.size.width as usize; | ||||
|         let format_size = image.texture_descriptor.format.pixel_size(); | ||||
|         render_queue.write_texture( | ||||
|             ImageCopyTexture { | ||||
|                 texture: &texture, | ||||
|                 mip_level: 0, | ||||
|                 origin: Origin3d::ZERO, | ||||
|             }, | ||||
|             &image.data, | ||||
|             ImageDataLayout { | ||||
|                 offset: 0, | ||||
|                 bytes_per_row: Some( | ||||
|                     std::num::NonZeroU32::new( | ||||
|                         image.texture_descriptor.size.width * format_size as u32, | ||||
|                     ) | ||||
|                     .unwrap(), | ||||
|                 ), | ||||
|                 rows_per_image: None, | ||||
|             }, | ||||
|             image.texture_descriptor.size, | ||||
|         ); | ||||
| 
 | ||||
|         let texture_view = texture.create_view(&TextureViewDescriptor::default()); | ||||
|         GpuImage { | ||||
|             texture, | ||||
|             texture_view, | ||||
|             sampler, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,27 +1,25 @@ | ||||
| #[cfg(feature = "hdr")] | ||||
| mod hdr_texture_loader; | ||||
| mod image_texture_loader; | ||||
| #[allow(clippy::module_inception)] | ||||
| mod texture; | ||||
| mod image; | ||||
| mod image_texture_loader; | ||||
| mod texture_cache; | ||||
| 
 | ||||
| pub(crate) mod image_texture_conversion; | ||||
| 
 | ||||
| #[cfg(feature = "hdr")] | ||||
| pub use hdr_texture_loader::*; | ||||
| pub use self::image::*; | ||||
| pub use image_texture_loader::*; | ||||
| pub use texture::*; | ||||
| pub use texture_cache::*; | ||||
| 
 | ||||
| use crate::{ | ||||
|     renderer::{RenderDevice, RenderQueue}, | ||||
|     render_asset::RenderAssetPlugin, | ||||
|     RenderStage, | ||||
| }; | ||||
| use bevy_app::{App, CoreStage, Plugin}; | ||||
| use bevy_asset::{AddAsset, AssetEvent, Assets}; | ||||
| use bevy_app::{App, Plugin}; | ||||
| use bevy_asset::{AddAsset}; | ||||
| use bevy_ecs::prelude::*; | ||||
| use bevy_utils::HashSet; | ||||
| use wgpu::{ImageCopyTexture, ImageDataLayout, Origin3d, TextureViewDescriptor}; | ||||
| 
 | ||||
| // TODO: replace Texture names with Image names?
 | ||||
| pub struct ImagePlugin; | ||||
| @ -33,7 +31,7 @@ impl Plugin for ImagePlugin { | ||||
|             app.init_asset_loader::<ImageTextureLoader>(); | ||||
|         } | ||||
| 
 | ||||
|         app.add_system_to_stage(CoreStage::PostUpdate, image_resource_system.system()) | ||||
|         app.add_plugin(RenderAssetPlugin::<Image>::default()) | ||||
|             .add_asset::<Image>(); | ||||
| 
 | ||||
|         let render_app = app.sub_app_mut(0); | ||||
| @ -43,93 +41,6 @@ impl Plugin for ImagePlugin { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn image_resource_system( | ||||
|     render_device: Res<RenderDevice>, | ||||
|     render_queue: Res<RenderQueue>, | ||||
|     mut images: ResMut<Assets<Image>>, | ||||
|     mut image_events: EventReader<AssetEvent<Image>>, | ||||
| ) { | ||||
|     let mut changed_images = HashSet::default(); | ||||
|     for event in image_events.iter() { | ||||
|         match event { | ||||
|             AssetEvent::Created { handle } => { | ||||
|                 changed_images.insert(handle); | ||||
|             } | ||||
|             AssetEvent::Modified { handle } => { | ||||
|                 changed_images.insert(handle); | ||||
|                 // TODO: uncomment this to support mutated textures
 | ||||
|                 // remove_current_texture_resources(render_resource_context, handle, &mut textures);
 | ||||
|             } | ||||
|             AssetEvent::Removed { handle } => { | ||||
|                 // if texture was modified and removed in the same update, ignore the
 | ||||
|                 // modification events are ordered so future modification
 | ||||
|                 // events are ok
 | ||||
|                 changed_images.remove(handle); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     for image_handle in changed_images.iter() { | ||||
|         if let Some(image) = images.get_mut(*image_handle) { | ||||
|             // TODO: this avoids creating new textures each frame because storing gpu data in the texture flags it as
 | ||||
|             // modified. this prevents hot reloading and therefore can't be used in an actual impl.
 | ||||
|             if image.gpu_data.is_some() { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             let texture = render_device.create_texture(&image.texture_descriptor); | ||||
|             let sampler = render_device.create_sampler(&image.sampler_descriptor); | ||||
| 
 | ||||
|             let width = image.texture_descriptor.size.width as usize; | ||||
|             let format_size = image.texture_descriptor.format.pixel_size(); | ||||
|             // let mut aligned_data = vec![
 | ||||
|             //     0;
 | ||||
|             //     format_size
 | ||||
|             //         * aligned_width
 | ||||
|             //         * image.texture_descriptor.size.height as usize
 | ||||
|             //         * image.texture_descriptor.size.depth_or_array_layers
 | ||||
|             //             as usize
 | ||||
|             // ];
 | ||||
|             // image
 | ||||
|             //     .data
 | ||||
|             //     .chunks_exact(format_size * width)
 | ||||
|             //     .enumerate()
 | ||||
|             //     .for_each(|(index, row)| {
 | ||||
|             //         let offset = index * aligned_width * format_size;
 | ||||
|             //         aligned_data[offset..(offset + width * format_size)].copy_from_slice(row);
 | ||||
|             //     });
 | ||||
| 
 | ||||
|             // TODO: this might require different alignment. docs seem to say that we don't need it though
 | ||||
|             render_queue.write_texture( | ||||
|                 ImageCopyTexture { | ||||
|                     texture: &texture, | ||||
|                     mip_level: 0, | ||||
|                     origin: Origin3d::ZERO, | ||||
|                 }, | ||||
|                 &image.data, | ||||
|                 ImageDataLayout { | ||||
|                     offset: 0, | ||||
|                     bytes_per_row: Some( | ||||
|                         std::num::NonZeroU32::new( | ||||
|                             image.texture_descriptor.size.width * format_size as u32, | ||||
|                         ) | ||||
|                         .unwrap(), | ||||
|                     ), | ||||
|                     rows_per_image: None, | ||||
|                 }, | ||||
|                 image.texture_descriptor.size, | ||||
|             ); | ||||
| 
 | ||||
|             let texture_view = texture.create_view(&TextureViewDescriptor::default()); | ||||
|             image.gpu_data = Some(ImageGpuData { | ||||
|                 texture, | ||||
|                 texture_view, | ||||
|                 sampler, | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub trait BevyDefault { | ||||
|     fn bevy_default() -> Self; | ||||
| } | ||||
|  | ||||
| @ -5,13 +5,14 @@ use bevy_math::{Mat4, Vec2, Vec3, Vec4Swizzles}; | ||||
| use bevy_render2::{ | ||||
|     core_pipeline::Transparent2dPhase, | ||||
|     mesh::{shape::Quad, Indices, Mesh, VertexAttributeValues}, | ||||
|     render_asset::RenderAssets, | ||||
|     render_graph::{Node, NodeRunError, RenderGraphContext}, | ||||
|     render_phase::{Draw, DrawFunctions, Drawable, RenderPhase, TrackedRenderPass}, | ||||
|     render_resource::*, | ||||
|     renderer::{RenderContext, RenderDevice}, | ||||
|     shader::Shader, | ||||
|     texture::{BevyDefault, Image}, | ||||
|     view::{ViewMeta, ViewUniformOffset, ViewUniform}, | ||||
|     view::{ViewMeta, ViewUniform, ViewUniformOffset}, | ||||
| }; | ||||
| use bevy_transform::components::GlobalTransform; | ||||
| use bevy_utils::HashMap; | ||||
| @ -48,8 +49,7 @@ impl FromWorld for SpriteShaders { | ||||
|             source: ShaderSource::SpirV(Cow::Borrowed(&fragment_spirv)), | ||||
|         }); | ||||
| 
 | ||||
|         let view_layout = | ||||
|             render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { | ||||
|         let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { | ||||
|             entries: &[BindGroupLayoutEntry { | ||||
|                 binding: 0, | ||||
|                 visibility: ShaderStage::VERTEX | ShaderStage::FRAGMENT, | ||||
| @ -57,9 +57,7 @@ impl FromWorld for SpriteShaders { | ||||
|                     ty: BufferBindingType::Uniform, | ||||
|                     has_dynamic_offset: true, | ||||
|                     // TODO: verify this is correct
 | ||||
|                         min_binding_size: BufferSize::new( | ||||
|                             std::mem::size_of::<ViewUniform>() as u64 | ||||
|                         ), | ||||
|                     min_binding_size: BufferSize::new(std::mem::size_of::<ViewUniform>() as u64), | ||||
|                 }, | ||||
|                 count: None, | ||||
|             }], | ||||
| @ -164,9 +162,7 @@ impl FromWorld for SpriteShaders { | ||||
| struct ExtractedSprite { | ||||
|     transform: Mat4, | ||||
|     size: Vec2, | ||||
|     // TODO: use asset handle here instead of owned renderer handles (lots of arc cloning)
 | ||||
|     texture_view: TextureView, | ||||
|     sampler: Sampler, | ||||
|     handle: Handle<Image>, | ||||
| } | ||||
| 
 | ||||
| pub struct ExtractedSprites { | ||||
| @ -175,22 +171,21 @@ pub struct ExtractedSprites { | ||||
| 
 | ||||
| pub fn extract_sprites( | ||||
|     mut commands: Commands, | ||||
|     textures: Res<Assets<Image>>, | ||||
|     images: Res<Assets<Image>>, | ||||
|     query: Query<(&Sprite, &GlobalTransform, &Handle<Image>)>, | ||||
| ) { | ||||
|     let mut extracted_sprites = Vec::new(); | ||||
|     for (sprite, transform, handle) in query.iter() { | ||||
|         if let Some(texture) = textures.get(handle) { | ||||
|             if let Some(gpu_data) = &texture.gpu_data { | ||||
|         if !images.contains(handle) { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         extracted_sprites.push(ExtractedSprite { | ||||
|             transform: transform.compute_matrix(), | ||||
|             size: sprite.size, | ||||
|                     texture_view: gpu_data.texture_view.clone(), | ||||
|                     sampler: gpu_data.sampler.clone(), | ||||
|             handle: handle.clone_weak(), | ||||
|         }) | ||||
|     } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     commands.insert_resource(ExtractedSprites { | ||||
|         sprites: extracted_sprites, | ||||
| @ -211,7 +206,7 @@ pub struct SpriteMeta { | ||||
|     view_bind_group: Option<BindGroup>, | ||||
|     // TODO: these should be garbage collected if unused across X frames
 | ||||
|     texture_bind_groups: Vec<BindGroup>, | ||||
|     texture_bind_group_indices: HashMap<TextureViewId, usize>, | ||||
|     texture_bind_group_indices: HashMap<Handle<Image>, usize>, | ||||
| } | ||||
| 
 | ||||
| impl Default for SpriteMeta { | ||||
| @ -309,6 +304,7 @@ pub fn queue_sprites( | ||||
|     view_meta: Res<ViewMeta>, | ||||
|     sprite_shaders: Res<SpriteShaders>, | ||||
|     extracted_sprites: Res<ExtractedSprites>, | ||||
|     gpu_images: Res<RenderAssets<Image>>, | ||||
|     mut views: Query<&mut RenderPhase<Transparent2dPhase>>, | ||||
| ) { | ||||
|     // TODO: define this without needing to check every frame
 | ||||
| @ -323,27 +319,25 @@ pub fn queue_sprites( | ||||
|         }) | ||||
|     }); | ||||
|     let sprite_meta = &mut *sprite_meta; | ||||
|     for mut transparent_phase in views.iter_mut() { | ||||
|         // TODO: free old bind groups? clear_unused_bind_groups() currently does this for us? Moving to RAII would also do this for us?
 | ||||
|     let draw_sprite_function = draw_functions.read().get_id::<DrawSprite>().unwrap(); | ||||
| 
 | ||||
|     for mut transparent_phase in views.iter_mut() { | ||||
|         let texture_bind_groups = &mut sprite_meta.texture_bind_groups; | ||||
|         // let material_layout = ;
 | ||||
|         for (i, sprite) in extracted_sprites.sprites.iter().enumerate() { | ||||
|             let bind_group_index = *sprite_meta | ||||
|                 .texture_bind_group_indices | ||||
|                 .entry(sprite.texture_view.id()) | ||||
|                 .entry(sprite.handle.clone_weak()) | ||||
|                 .or_insert_with(|| { | ||||
|                     let gpu_image = gpu_images.get(&sprite.handle).unwrap(); | ||||
|                     let index = texture_bind_groups.len(); | ||||
|                     let bind_group = render_device.create_bind_group(&BindGroupDescriptor { | ||||
|                         entries: &[ | ||||
|                             BindGroupEntry { | ||||
|                                 binding: 0, | ||||
|                                 resource: BindingResource::TextureView(&sprite.texture_view), | ||||
|                                 resource: BindingResource::TextureView(&gpu_image.texture_view), | ||||
|                             }, | ||||
|                             BindGroupEntry { | ||||
|                                 binding: 1, | ||||
|                                 resource: BindingResource::Sampler(&sprite.sampler), | ||||
|                                 resource: BindingResource::Sampler(&gpu_image.sampler), | ||||
|                             }, | ||||
|                         ], | ||||
|                         label: None, | ||||
| @ -410,10 +404,9 @@ impl Draw for DrawSprite { | ||||
|     ) { | ||||
|         const INDICES: usize = 6; | ||||
|         let (sprite_shaders, sprite_meta, views) = self.params.get(world); | ||||
|         let (sprite_shaders, sprite_meta, views) = | ||||
|             (sprite_shaders.into_inner(), sprite_meta.into_inner(), views); | ||||
|         let view_uniform = views.get(view).unwrap(); | ||||
|         pass.set_render_pipeline(&sprite_shaders.pipeline); | ||||
|         let sprite_meta = sprite_meta.into_inner(); | ||||
|         pass.set_render_pipeline(&sprite_shaders.into_inner().pipeline); | ||||
|         pass.set_vertex_buffer(0, sprite_meta.vertices.buffer().unwrap().slice(..)); | ||||
|         pass.set_index_buffer( | ||||
|             sprite_meta.indices.buffer().unwrap().slice(..), | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Carter Anderson
						Carter Anderson