AssetRenderResourceNodes now consume asset change events. Remove EntitiesWaitingForAssets in favor of DrawState.
This commit is contained in:
		
							parent
							
								
									2e48269923
								
							
						
					
					
						commit
						fc4160ea41
					
				@ -71,6 +71,7 @@ impl<T> Assets<T> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_mut(&mut self, handle: &Handle<T>) -> Option<&mut T> {
 | 
			
		||||
        self.events.send(AssetEvent::Modified { handle: *handle });
 | 
			
		||||
        self.assets.get_mut(&handle)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ use bevy_render::{
 | 
			
		||||
    base_render_graph,
 | 
			
		||||
    pipeline::PipelineDescriptor,
 | 
			
		||||
    render_graph::{
 | 
			
		||||
        nodes::{AssetUniformNode, UniformNode},
 | 
			
		||||
        nodes::{AssetRenderResourcesNode, RenderResourcesNode},
 | 
			
		||||
        RenderGraph,
 | 
			
		||||
    },
 | 
			
		||||
    shader::Shader,
 | 
			
		||||
@ -28,10 +28,10 @@ pub trait ForwardPbrRenderGraphBuilder {
 | 
			
		||||
 | 
			
		||||
impl ForwardPbrRenderGraphBuilder for RenderGraph {
 | 
			
		||||
    fn add_pbr_graph(&mut self, resources: &Resources) -> &mut Self {
 | 
			
		||||
        self.add_system_node(node::TRANSFORM, UniformNode::<Transform>::new(true));
 | 
			
		||||
        self.add_system_node(node::TRANSFORM, RenderResourcesNode::<Transform>::new(true));
 | 
			
		||||
        self.add_system_node(
 | 
			
		||||
            node::STANDARD_MATERIAL,
 | 
			
		||||
            AssetUniformNode::<StandardMaterial>::new(true),
 | 
			
		||||
            AssetRenderResourcesNode::<StandardMaterial>::new(true),
 | 
			
		||||
        );
 | 
			
		||||
        self.add_system_node(node::LIGHTS, LightsNode::new(10));
 | 
			
		||||
        let mut shaders = resources.get_mut::<Assets<Shader>>().unwrap();
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,6 @@ use draw::{clear_draw_system, Draw, RenderPipelines};
 | 
			
		||||
use legion::prelude::IntoSystem;
 | 
			
		||||
use mesh::mesh_resource_provider_system;
 | 
			
		||||
use render_graph::RenderGraph;
 | 
			
		||||
use render_resource::EntitiesWaitingForAssets;
 | 
			
		||||
use std::ops::Range;
 | 
			
		||||
use texture::{PngTextureLoader, TextureResourceSystemState};
 | 
			
		||||
 | 
			
		||||
@ -83,7 +82,6 @@ impl AppPlugin for RenderPlugin {
 | 
			
		||||
            .init_resource::<PipelineCompiler>()
 | 
			
		||||
            .init_resource::<RenderResourceAssignments>()
 | 
			
		||||
            .init_resource::<VertexBufferDescriptors>()
 | 
			
		||||
            .init_resource::<EntitiesWaitingForAssets>()
 | 
			
		||||
            .init_resource::<TextureResourceSystemState>()
 | 
			
		||||
            .add_system_to_stage(bevy_app::stage::PRE_UPDATE, clear_draw_system.system())
 | 
			
		||||
            .init_system_to_stage(
 | 
			
		||||
@ -94,10 +92,6 @@ impl AppPlugin for RenderPlugin {
 | 
			
		||||
                bevy_app::stage::POST_UPDATE,
 | 
			
		||||
                camera::camera_system::<PerspectiveProjection>,
 | 
			
		||||
            )
 | 
			
		||||
            .add_system_to_stage(
 | 
			
		||||
                bevy_app::stage::PRE_UPDATE,
 | 
			
		||||
                EntitiesWaitingForAssets::clear_system.system(),
 | 
			
		||||
            )
 | 
			
		||||
            .init_system_to_stage(stage::RENDER_RESOURCE, mesh_resource_provider_system)
 | 
			
		||||
            .add_system_to_stage(
 | 
			
		||||
                stage::RENDER_RESOURCE,
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,13 @@
 | 
			
		||||
mod camera_node;
 | 
			
		||||
mod pass_node;
 | 
			
		||||
mod texture_copy_node;
 | 
			
		||||
mod uniform_node;
 | 
			
		||||
mod render_resources_node;
 | 
			
		||||
mod window_swapchain_node;
 | 
			
		||||
mod window_texture_node;
 | 
			
		||||
 | 
			
		||||
pub use camera_node::*;
 | 
			
		||||
pub use pass_node::*;
 | 
			
		||||
pub use texture_copy_node::*;
 | 
			
		||||
pub use uniform_node::*;
 | 
			
		||||
pub use render_resources_node::*;
 | 
			
		||||
pub use window_swapchain_node::*;
 | 
			
		||||
pub use window_texture_node::*;
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,14 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    draw::{Draw, RenderCommand},
 | 
			
		||||
    pass::{PassDescriptor, TextureAttachment},
 | 
			
		||||
    pipeline::PipelineDescriptor,
 | 
			
		||||
    render_graph::{Node, ResourceSlotInfo, ResourceSlots},
 | 
			
		||||
    render_resource::{EntitiesWaitingForAssets, RenderResourceAssignments, ResourceInfo},
 | 
			
		||||
    render_resource::{
 | 
			
		||||
        RenderResourceAssignments, RenderResourceId, RenderResourceSetId, ResourceInfo,
 | 
			
		||||
    },
 | 
			
		||||
    renderer::RenderContext,
 | 
			
		||||
};
 | 
			
		||||
use bevy_asset::{Assets, Handle};
 | 
			
		||||
use legion::prelude::*;
 | 
			
		||||
 | 
			
		||||
pub struct MainPassNode {
 | 
			
		||||
@ -63,8 +67,8 @@ impl Node for MainPassNode {
 | 
			
		||||
        input: &ResourceSlots,
 | 
			
		||||
        _output: &mut ResourceSlots,
 | 
			
		||||
    ) {
 | 
			
		||||
        let entities_waiting_for_assets = resources.get::<EntitiesWaitingForAssets>().unwrap();
 | 
			
		||||
        let render_resource_assignments = resources.get::<RenderResourceAssignments>().unwrap();
 | 
			
		||||
        let pipelines = resources.get::<Assets<PipelineDescriptor>>().unwrap();
 | 
			
		||||
 | 
			
		||||
        for (i, color_attachment) in self.descriptor.color_attachments.iter_mut().enumerate() {
 | 
			
		||||
            if let Some(input_index) = self.color_attachment_input_indices[i] {
 | 
			
		||||
@ -85,8 +89,9 @@ impl Node for MainPassNode {
 | 
			
		||||
            &self.descriptor,
 | 
			
		||||
            &render_resource_assignments,
 | 
			
		||||
            &mut |render_pass| {
 | 
			
		||||
                for (entity, draw) in <Read<Draw>>::query().iter_entities(&world) {
 | 
			
		||||
                    if !draw.is_visible || entities_waiting_for_assets.contains(&entity) {
 | 
			
		||||
                let mut draw_state = DrawState::default();
 | 
			
		||||
                for draw in <Read<Draw>>::query().iter(&world) {
 | 
			
		||||
                    if !draw.is_visible {
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@ -95,17 +100,23 @@ impl Node for MainPassNode {
 | 
			
		||||
                            RenderCommand::SetPipeline { pipeline } => {
 | 
			
		||||
                                // TODO: Filter pipelines
 | 
			
		||||
                                render_pass.set_pipeline(*pipeline);
 | 
			
		||||
                                let descriptor = pipelines.get(pipeline).unwrap();
 | 
			
		||||
                                draw_state.set_pipeline(*pipeline, descriptor);
 | 
			
		||||
                            }
 | 
			
		||||
                            RenderCommand::DrawIndexed {
 | 
			
		||||
                                base_vertex,
 | 
			
		||||
                                indices,
 | 
			
		||||
                                instances,
 | 
			
		||||
                            } => {
 | 
			
		||||
                                render_pass.draw_indexed(
 | 
			
		||||
                                    indices.clone(),
 | 
			
		||||
                                    *base_vertex,
 | 
			
		||||
                                    instances.clone(),
 | 
			
		||||
                                );
 | 
			
		||||
                                if draw_state.can_draw_indexed() {
 | 
			
		||||
                                    render_pass.draw_indexed(
 | 
			
		||||
                                        indices.clone(),
 | 
			
		||||
                                        *base_vertex,
 | 
			
		||||
                                        instances.clone(),
 | 
			
		||||
                                    );
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    log::info!("Could not draw indexed because the pipeline layout wasn't fully set for pipeline: {:?}", draw_state.pipeline);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            RenderCommand::SetVertexBuffer {
 | 
			
		||||
                                buffer,
 | 
			
		||||
@ -113,9 +124,11 @@ impl Node for MainPassNode {
 | 
			
		||||
                                slot,
 | 
			
		||||
                            } => {
 | 
			
		||||
                                render_pass.set_vertex_buffer(*slot, *buffer, *offset);
 | 
			
		||||
                                draw_state.set_vertex_buffer(*slot, *buffer);
 | 
			
		||||
                            }
 | 
			
		||||
                            RenderCommand::SetIndexBuffer { buffer, offset } => {
 | 
			
		||||
                                render_pass.set_index_buffer(*buffer, *offset);
 | 
			
		||||
                                draw_state.set_index_buffer(*buffer)
 | 
			
		||||
                            }
 | 
			
		||||
                            RenderCommand::SetBindGroup {
 | 
			
		||||
                                index,
 | 
			
		||||
@ -131,6 +144,7 @@ impl Node for MainPassNode {
 | 
			
		||||
                                        .as_ref()
 | 
			
		||||
                                        .map(|indices| indices.as_slice()),
 | 
			
		||||
                                );
 | 
			
		||||
                                draw_state.set_bind_group(*index, *render_resource_set);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@ -139,3 +153,48 @@ impl Node for MainPassNode {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tracks the current pipeline state to ensure draw calls are valid.
 | 
			
		||||
#[derive(Default)]
 | 
			
		||||
struct DrawState {
 | 
			
		||||
    pipeline: Option<Handle<PipelineDescriptor>>,
 | 
			
		||||
    bind_groups: Vec<Option<RenderResourceSetId>>,
 | 
			
		||||
    vertex_buffers: Vec<Option<RenderResourceId>>,
 | 
			
		||||
    index_buffer: Option<RenderResourceId>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl DrawState {
 | 
			
		||||
    pub fn set_bind_group(&mut self, index: u32, render_resource_set: RenderResourceSetId) {
 | 
			
		||||
        self.bind_groups[index as usize] = Some(render_resource_set);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_vertex_buffer(&mut self, index: u32, buffer: RenderResourceId) {
 | 
			
		||||
        self.vertex_buffers[index as usize] = Some(buffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_index_buffer(&mut self, buffer: RenderResourceId) {
 | 
			
		||||
        self.index_buffer = Some(buffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn can_draw_indexed(&self) -> bool {
 | 
			
		||||
        self.bind_groups.iter().all(|b| b.is_some())
 | 
			
		||||
            && self.vertex_buffers.iter().all(|v| v.is_some())
 | 
			
		||||
            && self.index_buffer.is_some()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_pipeline(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        handle: Handle<PipelineDescriptor>,
 | 
			
		||||
        descriptor: &PipelineDescriptor,
 | 
			
		||||
    ) {
 | 
			
		||||
        self.bind_groups.clear();
 | 
			
		||||
        self.vertex_buffers.clear();
 | 
			
		||||
        self.index_buffer = None;
 | 
			
		||||
 | 
			
		||||
        self.pipeline = Some(handle);
 | 
			
		||||
        let layout = descriptor.get_layout().unwrap();
 | 
			
		||||
        self.bind_groups.resize(layout.bind_groups.len(), None);
 | 
			
		||||
        self.vertex_buffers
 | 
			
		||||
            .resize(layout.vertex_buffer_descriptors.len(), None);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,19 +2,21 @@ use crate::{
 | 
			
		||||
    draw::{Draw, RenderPipelines},
 | 
			
		||||
    render_graph::{CommandQueue, Node, ResourceSlots, SystemNode},
 | 
			
		||||
    render_resource::{
 | 
			
		||||
        self, BufferInfo, BufferUsage, EntitiesWaitingForAssets, RenderResourceAssignment,
 | 
			
		||||
        RenderResourceAssignments, RenderResourceAssignmentsId, RenderResourceHints,
 | 
			
		||||
        RenderResourceId,
 | 
			
		||||
        self, BufferInfo, BufferUsage, RenderResourceAssignment, RenderResourceAssignments,
 | 
			
		||||
        RenderResourceAssignmentsId, RenderResourceHints, RenderResourceId,
 | 
			
		||||
    },
 | 
			
		||||
    renderer::{RenderContext, RenderResourceContext, RenderResources},
 | 
			
		||||
    texture,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use bevy_app::EventReader;
 | 
			
		||||
use bevy_app::{EventReader, Events};
 | 
			
		||||
use bevy_asset::{AssetEvent, Assets, Handle};
 | 
			
		||||
use legion::prelude::*;
 | 
			
		||||
use render_resource::ResourceInfo;
 | 
			
		||||
use std::{collections::HashMap, marker::PhantomData};
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::{HashMap, HashSet},
 | 
			
		||||
    marker::PhantomData,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub const BIND_BUFFER_ALIGNMENT: usize = 256;
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
@ -25,7 +27,7 @@ struct QueuedBufferWrite {
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
struct BufferArrayStatus {
 | 
			
		||||
    new_item_count: usize,
 | 
			
		||||
    changed_item_count: usize,
 | 
			
		||||
    item_size: usize,
 | 
			
		||||
    aligned_size: usize,
 | 
			
		||||
    staging_buffer_offset: usize,
 | 
			
		||||
@ -78,15 +80,15 @@ impl<T> UniformBufferArrays<T>
 | 
			
		||||
where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
{
 | 
			
		||||
    fn reset_new_item_counts(&mut self) {
 | 
			
		||||
    fn reset_changed_item_counts(&mut self) {
 | 
			
		||||
        for buffer_status in self.uniform_arrays.iter_mut() {
 | 
			
		||||
            if let Some((_name, buffer_status)) = buffer_status {
 | 
			
		||||
                buffer_status.new_item_count = 0;
 | 
			
		||||
                buffer_status.changed_item_count = 0;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn increment_uniform_counts(&mut self, uniforms: &T) {
 | 
			
		||||
    fn increment_changed_item_counts(&mut self, uniforms: &T) {
 | 
			
		||||
        if self.uniform_arrays.len() != uniforms.render_resources_len() {
 | 
			
		||||
            self.uniform_arrays
 | 
			
		||||
                .resize_with(uniforms.render_resources_len(), || None);
 | 
			
		||||
@ -96,12 +98,12 @@ where
 | 
			
		||||
                let render_resource_name = uniforms.get_render_resource_name(i).unwrap();
 | 
			
		||||
                let size = render_resource.buffer_byte_len().unwrap();
 | 
			
		||||
                if let Some((ref _name, ref mut buffer_array_status)) = self.uniform_arrays[i] {
 | 
			
		||||
                    buffer_array_status.new_item_count += 1;
 | 
			
		||||
                    buffer_array_status.changed_item_count += 1;
 | 
			
		||||
                } else {
 | 
			
		||||
                    self.uniform_arrays[i] = Some((
 | 
			
		||||
                        render_resource_name.to_string(),
 | 
			
		||||
                        BufferArrayStatus {
 | 
			
		||||
                            new_item_count: 1,
 | 
			
		||||
                            changed_item_count: 1,
 | 
			
		||||
                            queued_buffer_writes: Vec::new(),
 | 
			
		||||
                            aligned_size: Self::get_aligned_dynamic_uniform_size(size),
 | 
			
		||||
                            item_size: size,
 | 
			
		||||
@ -134,7 +136,7 @@ where
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                buffer_array_status.queued_buffer_writes =
 | 
			
		||||
                    Vec::with_capacity(buffer_array_status.new_item_count);
 | 
			
		||||
                    Vec::with_capacity(buffer_array_status.changed_item_count);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -144,9 +146,9 @@ where
 | 
			
		||||
        render_resource_context: &dyn RenderResourceContext,
 | 
			
		||||
        align: bool,
 | 
			
		||||
    ) {
 | 
			
		||||
        if buffer_array_status.current_item_capacity < buffer_array_status.new_item_count {
 | 
			
		||||
        if buffer_array_status.current_item_capacity < buffer_array_status.changed_item_count {
 | 
			
		||||
            let new_capacity =
 | 
			
		||||
                buffer_array_status.new_item_count + buffer_array_status.new_item_count / 2;
 | 
			
		||||
                buffer_array_status.changed_item_count + buffer_array_status.changed_item_count / 2;
 | 
			
		||||
            let mut item_size = buffer_array_status.item_size;
 | 
			
		||||
            if align {
 | 
			
		||||
                item_size = Self::get_aligned_dynamic_uniform_size(item_size);
 | 
			
		||||
@ -177,7 +179,7 @@ where
 | 
			
		||||
        for dynamic_buffer_array_status in self.uniform_arrays.iter_mut() {
 | 
			
		||||
            if let Some((_name, ref mut buffer_array_status)) = dynamic_buffer_array_status {
 | 
			
		||||
                buffer_array_status.staging_buffer_offset = size;
 | 
			
		||||
                size += buffer_array_status.item_size * buffer_array_status.new_item_count;
 | 
			
		||||
                size += buffer_array_status.item_size * buffer_array_status.changed_item_count;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -298,7 +300,7 @@ where
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default)]
 | 
			
		||||
pub struct UniformNode<T>
 | 
			
		||||
pub struct RenderResourcesNode<T>
 | 
			
		||||
where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
{
 | 
			
		||||
@ -307,12 +309,12 @@ where
 | 
			
		||||
    _marker: PhantomData<T>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> UniformNode<T>
 | 
			
		||||
impl<T> RenderResourcesNode<T>
 | 
			
		||||
where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
{
 | 
			
		||||
    pub fn new(dynamic_uniforms: bool) -> Self {
 | 
			
		||||
        UniformNode {
 | 
			
		||||
        RenderResourcesNode {
 | 
			
		||||
            command_queue: CommandQueue::default(),
 | 
			
		||||
            dynamic_uniforms,
 | 
			
		||||
            _marker: PhantomData::default(),
 | 
			
		||||
@ -320,7 +322,7 @@ where
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> Node for UniformNode<T>
 | 
			
		||||
impl<T> Node for RenderResourcesNode<T>
 | 
			
		||||
where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
{
 | 
			
		||||
@ -336,7 +338,7 @@ where
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> SystemNode for UniformNode<T>
 | 
			
		||||
impl<T> SystemNode for RenderResourcesNode<T>
 | 
			
		||||
where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
{
 | 
			
		||||
@ -347,35 +349,30 @@ where
 | 
			
		||||
        // TODO: maybe run "update" here
 | 
			
		||||
        (move |world: &mut SubWorld,
 | 
			
		||||
               render_resources: Res<RenderResources>,
 | 
			
		||||
               entities_waiting_for_assets: Res<EntitiesWaitingForAssets>,
 | 
			
		||||
               query: &mut Query<(Read<T>, Read<Draw>, Write<RenderPipelines>)>| {
 | 
			
		||||
            let render_resource_context = &*render_resources.context;
 | 
			
		||||
 | 
			
		||||
            uniform_buffer_arrays.reset_new_item_counts();
 | 
			
		||||
            uniform_buffer_arrays.reset_changed_item_counts();
 | 
			
		||||
            // update uniforms info
 | 
			
		||||
            for (uniforms, draw, _render_pipelines) in query.iter_mut(world) {
 | 
			
		||||
                if !draw.is_visible {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                uniform_buffer_arrays.increment_uniform_counts(&uniforms);
 | 
			
		||||
                uniform_buffer_arrays.increment_changed_item_counts(&uniforms);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            uniform_buffer_arrays.setup_buffer_arrays(render_resource_context, dynamic_uniforms);
 | 
			
		||||
            let staging_buffer_size = uniform_buffer_arrays.update_staging_buffer_offsets();
 | 
			
		||||
 | 
			
		||||
            for (entity, (uniforms, draw, mut render_pipelines)) in
 | 
			
		||||
                query.iter_entities_mut(world)
 | 
			
		||||
            {
 | 
			
		||||
            for (uniforms, draw, mut render_pipelines) in query.iter_mut(world) {
 | 
			
		||||
                if !draw.is_visible {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setup_uniform_texture_resources::<T>(
 | 
			
		||||
                    entity,
 | 
			
		||||
                    &uniforms,
 | 
			
		||||
                    render_resource_context,
 | 
			
		||||
                    &entities_waiting_for_assets,
 | 
			
		||||
                    &mut render_pipelines.render_resource_assignments,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
@ -403,9 +400,7 @@ where
 | 
			
		||||
                        ..Default::default()
 | 
			
		||||
                    },
 | 
			
		||||
                    &mut |mut staging_buffer, _render_resources| {
 | 
			
		||||
                        for (uniforms, draw, mut render_pipelines) in
 | 
			
		||||
                            query.iter_mut(world)
 | 
			
		||||
                        {
 | 
			
		||||
                        for (uniforms, draw, mut render_pipelines) in query.iter_mut(world) {
 | 
			
		||||
                            if !draw.is_visible {
 | 
			
		||||
                                return;
 | 
			
		||||
                            }
 | 
			
		||||
@ -431,7 +426,7 @@ where
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default)]
 | 
			
		||||
pub struct AssetUniformNode<T>
 | 
			
		||||
pub struct AssetRenderResourcesNode<T>
 | 
			
		||||
where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
{
 | 
			
		||||
@ -440,12 +435,12 @@ where
 | 
			
		||||
    _marker: PhantomData<T>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> AssetUniformNode<T>
 | 
			
		||||
impl<T> AssetRenderResourcesNode<T>
 | 
			
		||||
where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
{
 | 
			
		||||
    pub fn new(dynamic_uniforms: bool) -> Self {
 | 
			
		||||
        AssetUniformNode {
 | 
			
		||||
        AssetRenderResourcesNode {
 | 
			
		||||
            dynamic_uniforms,
 | 
			
		||||
            command_queue: Default::default(),
 | 
			
		||||
            _marker: Default::default(),
 | 
			
		||||
@ -453,7 +448,7 @@ where
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> Node for AssetUniformNode<T>
 | 
			
		||||
impl<T> Node for AssetRenderResourcesNode<T>
 | 
			
		||||
where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
{
 | 
			
		||||
@ -469,72 +464,79 @@ where
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> SystemNode for AssetUniformNode<T>
 | 
			
		||||
const EXPECT_ASSET_MESSAGE: &str = "Only assets that exist should be in the modified assets list";
 | 
			
		||||
 | 
			
		||||
impl<T> SystemNode for AssetRenderResourcesNode<T>
 | 
			
		||||
where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
{
 | 
			
		||||
    fn get_system(&self) -> Box<dyn Schedulable> {
 | 
			
		||||
        let mut command_queue = self.command_queue.clone();
 | 
			
		||||
        let mut uniform_buffer_arrays = UniformBufferArrays::<T>::default();
 | 
			
		||||
        let mut asset_event_reader = EventReader::<AssetEvent<T>>::default();
 | 
			
		||||
        let mut asset_render_resource_assignments =
 | 
			
		||||
            HashMap::<Handle<T>, RenderResourceAssignments>::default();
 | 
			
		||||
        let dynamic_uniforms = self.dynamic_uniforms;
 | 
			
		||||
        (move |world: &mut SubWorld,
 | 
			
		||||
          assets: Res<Assets<T>>,
 | 
			
		||||
          render_resources: Res<RenderResources>,
 | 
			
		||||
          entities_waiting_for_assets: Res<EntitiesWaitingForAssets>,
 | 
			
		||||
          query: &mut Query<(Read<Handle<T>>, Read<Draw>, Write<RenderPipelines>)>| {
 | 
			
		||||
               assets: Res<Assets<T>>,
 | 
			
		||||
               asset_events: Res<Events<AssetEvent<T>>>,
 | 
			
		||||
               render_resources: Res<RenderResources>,
 | 
			
		||||
               query: &mut Query<(Read<Handle<T>>, Read<Draw>, Write<RenderPipelines>)>| {
 | 
			
		||||
            let render_resource_context = &*render_resources.context;
 | 
			
		||||
            uniform_buffer_arrays.reset_new_item_counts();
 | 
			
		||||
            uniform_buffer_arrays.reset_changed_item_counts();
 | 
			
		||||
 | 
			
		||||
            let mut modified_assets = HashSet::new();
 | 
			
		||||
            for event in asset_event_reader.iter(&asset_events) {
 | 
			
		||||
                match event {
 | 
			
		||||
                    AssetEvent::Created { handle } => {
 | 
			
		||||
                        modified_assets.insert(*handle);
 | 
			
		||||
                    }
 | 
			
		||||
                    AssetEvent::Modified { handle } => {
 | 
			
		||||
                        modified_assets.insert(*handle);
 | 
			
		||||
                    }
 | 
			
		||||
                    AssetEvent::Removed { handle } => {
 | 
			
		||||
                        // TODO: handle removals
 | 
			
		||||
                        modified_assets.remove(handle);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // update uniform handles info
 | 
			
		||||
            for (entity, (handle, draw, _render_pipelines)) in query.iter_entities_mut(world) {
 | 
			
		||||
                if !draw.is_visible {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if let Some(uniforms) = assets.get(&handle) {
 | 
			
		||||
                    // TODO: only increment count if we haven't seen this uniform handle before
 | 
			
		||||
                    uniform_buffer_arrays.increment_uniform_counts(&uniforms);
 | 
			
		||||
                } else {
 | 
			
		||||
                    entities_waiting_for_assets.add(entity)
 | 
			
		||||
                }
 | 
			
		||||
            for asset_handle in modified_assets.iter() {
 | 
			
		||||
                let asset = assets.get(&asset_handle).expect(EXPECT_ASSET_MESSAGE);
 | 
			
		||||
                uniform_buffer_arrays.increment_changed_item_counts(&asset);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            uniform_buffer_arrays.setup_buffer_arrays(render_resource_context, dynamic_uniforms);
 | 
			
		||||
            let staging_buffer_size = uniform_buffer_arrays.update_staging_buffer_offsets();
 | 
			
		||||
 | 
			
		||||
            for (entity, (handle, draw, mut render_pipelines)) in
 | 
			
		||||
                query.iter_entities_mut(world)
 | 
			
		||||
            {
 | 
			
		||||
                if !draw.is_visible {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if let Some(uniforms) = assets.get(&handle) {
 | 
			
		||||
                    setup_uniform_texture_resources::<T>(
 | 
			
		||||
                        entity,
 | 
			
		||||
                        &uniforms,
 | 
			
		||||
                        render_resource_context,
 | 
			
		||||
                        &entities_waiting_for_assets,
 | 
			
		||||
                        &mut render_pipelines.render_resource_assignments,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            for asset_handle in modified_assets.iter() {
 | 
			
		||||
                let asset = assets.get(&asset_handle).expect(EXPECT_ASSET_MESSAGE);
 | 
			
		||||
                let mut render_resource_assignments = asset_render_resource_assignments
 | 
			
		||||
                    .entry(*asset_handle)
 | 
			
		||||
                    .or_insert_with(|| RenderResourceAssignments::default());
 | 
			
		||||
                setup_uniform_texture_resources::<T>(
 | 
			
		||||
                    &asset,
 | 
			
		||||
                    render_resource_context,
 | 
			
		||||
                    &mut render_resource_assignments,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if staging_buffer_size == 0 {
 | 
			
		||||
                let mut staging_buffer: [u8; 0] = [];
 | 
			
		||||
                for (handle, draw, mut render_pipelines) in query.iter_mut(world) {
 | 
			
		||||
                    if !draw.is_visible {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    if let Some(uniforms) = assets.get(&handle) {
 | 
			
		||||
                        // TODO: only setup buffer if we haven't seen this handle before
 | 
			
		||||
                        uniform_buffer_arrays.setup_uniform_buffer_resources(
 | 
			
		||||
                            &uniforms,
 | 
			
		||||
                            dynamic_uniforms,
 | 
			
		||||
                            render_resource_context,
 | 
			
		||||
                            &mut render_pipelines.render_resource_assignments,
 | 
			
		||||
                            &mut staging_buffer,
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                for asset_handle in modified_assets.iter() {
 | 
			
		||||
                    let asset = assets.get(&asset_handle).expect(EXPECT_ASSET_MESSAGE);
 | 
			
		||||
                    let mut render_resource_assignments = asset_render_resource_assignments
 | 
			
		||||
                        .entry(*asset_handle)
 | 
			
		||||
                        .or_insert_with(|| RenderResourceAssignments::default());
 | 
			
		||||
                    // TODO: only setup buffer if we haven't seen this handle before
 | 
			
		||||
                    uniform_buffer_arrays.setup_uniform_buffer_resources(
 | 
			
		||||
                        &asset,
 | 
			
		||||
                        dynamic_uniforms,
 | 
			
		||||
                        render_resource_context,
 | 
			
		||||
                        &mut render_resource_assignments,
 | 
			
		||||
                        &mut staging_buffer,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                let staging_buffer = render_resource_context.create_buffer_mapped(
 | 
			
		||||
@ -544,20 +546,19 @@ where
 | 
			
		||||
                        ..Default::default()
 | 
			
		||||
                    },
 | 
			
		||||
                    &mut |mut staging_buffer, _render_resources| {
 | 
			
		||||
                        for (handle, draw, mut render_pipelines) in query.iter_mut(world) {
 | 
			
		||||
                            if !draw.is_visible {
 | 
			
		||||
                                return;
 | 
			
		||||
                            }
 | 
			
		||||
                            if let Some(uniforms) = assets.get(&handle) {
 | 
			
		||||
                                // TODO: only setup buffer if we haven't seen this handle before
 | 
			
		||||
                                uniform_buffer_arrays.setup_uniform_buffer_resources(
 | 
			
		||||
                                    &uniforms,
 | 
			
		||||
                                    dynamic_uniforms,
 | 
			
		||||
                                    render_resource_context,
 | 
			
		||||
                                    &mut render_pipelines.render_resource_assignments,
 | 
			
		||||
                                    &mut staging_buffer,
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                        for asset_handle in modified_assets.iter() {
 | 
			
		||||
                            let asset = assets.get(&asset_handle).expect(EXPECT_ASSET_MESSAGE);
 | 
			
		||||
                            let mut render_resource_assignments = asset_render_resource_assignments
 | 
			
		||||
                                .entry(*asset_handle)
 | 
			
		||||
                                .or_insert_with(|| RenderResourceAssignments::default());
 | 
			
		||||
                            // TODO: only setup buffer if we haven't seen this handle before
 | 
			
		||||
                            uniform_buffer_arrays.setup_uniform_buffer_resources(
 | 
			
		||||
                                &asset,
 | 
			
		||||
                                dynamic_uniforms,
 | 
			
		||||
                                render_resource_context,
 | 
			
		||||
                                &mut render_resource_assignments,
 | 
			
		||||
                                &mut staging_buffer,
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                );
 | 
			
		||||
@ -566,16 +567,20 @@ where
 | 
			
		||||
                    .copy_staging_buffer_to_final_buffers(&mut command_queue, staging_buffer);
 | 
			
		||||
                command_queue.free_buffer(staging_buffer);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (asset_handle, _draw, mut render_pipelines) in query.iter_mut(world) {
 | 
			
		||||
                if let Some(asset_assignments) = asset_render_resource_assignments.get(&asset_handle) {
 | 
			
		||||
                    render_pipelines.render_resource_assignments.extend(asset_assignments);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        .system()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn setup_uniform_texture_resources<T>(
 | 
			
		||||
    entity: Entity,
 | 
			
		||||
    uniforms: &T,
 | 
			
		||||
    render_resource_context: &dyn RenderResourceContext,
 | 
			
		||||
    entities_waiting_for_assets: &EntitiesWaitingForAssets,
 | 
			
		||||
    render_resource_assignments: &mut RenderResourceAssignments,
 | 
			
		||||
) where
 | 
			
		||||
    T: render_resource::RenderResources,
 | 
			
		||||
@ -600,11 +605,7 @@ fn setup_uniform_texture_resources<T>(
 | 
			
		||||
                        RenderResourceAssignment::Sampler(sampler_resource),
 | 
			
		||||
                    );
 | 
			
		||||
                    continue;
 | 
			
		||||
                } else {
 | 
			
		||||
                    entities_waiting_for_assets.add(entity);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                entities_waiting_for_assets.add(entity);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -1,31 +0,0 @@
 | 
			
		||||
use legion::prelude::{Entity, Res};
 | 
			
		||||
use std::{collections::HashSet, sync::RwLock};
 | 
			
		||||
 | 
			
		||||
#[derive(Default)]
 | 
			
		||||
pub struct EntitiesWaitingForAssets {
 | 
			
		||||
    pub entities: RwLock<HashSet<Entity>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EntitiesWaitingForAssets {
 | 
			
		||||
    pub fn add(&self, entity: Entity) {
 | 
			
		||||
        self.entities
 | 
			
		||||
            .write()
 | 
			
		||||
            .expect("RwLock poisoned")
 | 
			
		||||
            .insert(entity);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn contains(&self, entity: &Entity) -> bool {
 | 
			
		||||
        self.entities
 | 
			
		||||
            .read()
 | 
			
		||||
            .expect("RwLock poisoned")
 | 
			
		||||
            .contains(entity)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn clear(&self) {
 | 
			
		||||
        self.entities.write().expect("RwLock poisoned").clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn clear_system(entities_waiting_for_assets: Res<EntitiesWaitingForAssets>) {
 | 
			
		||||
        entities_waiting_for_assets.clear();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
mod buffer;
 | 
			
		||||
mod entities_waiting_for_assets;
 | 
			
		||||
mod render_resource;
 | 
			
		||||
mod render_resource_set;
 | 
			
		||||
mod render_resource_assignments;
 | 
			
		||||
@ -7,7 +6,6 @@ mod resource_info;
 | 
			
		||||
mod systems;
 | 
			
		||||
 | 
			
		||||
pub use buffer::*;
 | 
			
		||||
pub use entities_waiting_for_assets::*;
 | 
			
		||||
pub use render_resource::*;
 | 
			
		||||
pub use render_resource_set::*;
 | 
			
		||||
pub use render_resource_assignments::*;
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ use super::{RenderResourceId, RenderResourceSet, RenderResourceSetId};
 | 
			
		||||
use crate::pipeline::{BindGroupDescriptor, BindGroupDescriptorId, PipelineSpecialization};
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::{HashMap, HashSet},
 | 
			
		||||
    hash::{Hash, Hasher},
 | 
			
		||||
    hash::Hash,
 | 
			
		||||
    ops::Range,
 | 
			
		||||
};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
@ -60,6 +60,7 @@ impl RenderResourceAssignments {
 | 
			
		||||
    fn try_set_dirty(&mut self, name: &str, assignment: &RenderResourceAssignment) {
 | 
			
		||||
        if let Some(current_assignment) = self.render_resources.get(name) {
 | 
			
		||||
            if current_assignment != assignment {
 | 
			
		||||
                // TODO: this is crude. we shouldn't need to invalidate all render resource sets
 | 
			
		||||
                for id in self.render_resource_sets.keys() {
 | 
			
		||||
                    self.dirty_render_resource_sets.insert(*id);
 | 
			
		||||
                }
 | 
			
		||||
@ -67,6 +68,18 @@ impl RenderResourceAssignments {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn extend(&mut self, render_resource_assignments: &RenderResourceAssignments) {
 | 
			
		||||
        for (name, assignment) in render_resource_assignments.render_resources.iter() {
 | 
			
		||||
            self.set(name, assignment.clone());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (name, (vertex_buffer, index_buffer)) in
 | 
			
		||||
            render_resource_assignments.vertex_buffers.iter()
 | 
			
		||||
        {
 | 
			
		||||
            self.set_vertex_buffer(name, *vertex_buffer, index_buffer.clone());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_vertex_buffer(
 | 
			
		||||
        &self,
 | 
			
		||||
        name: &str,
 | 
			
		||||
 | 
			
		||||
@ -16,10 +16,10 @@ pub struct IndexedRenderResourceAssignment {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: consider renaming this to BindGroup for parity with renderer terminology
 | 
			
		||||
#[derive(Eq, PartialEq, Debug)]
 | 
			
		||||
#[derive(Clone, Eq, PartialEq, Debug)]
 | 
			
		||||
pub struct RenderResourceSet {
 | 
			
		||||
    pub id: RenderResourceSetId,
 | 
			
		||||
    pub indexed_assignments: Vec<IndexedRenderResourceAssignment>,
 | 
			
		||||
    pub indexed_assignments: Arc<Vec<IndexedRenderResourceAssignment>>,
 | 
			
		||||
    pub dynamic_uniform_indices: Option<Arc<Vec<u32>>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -99,7 +99,7 @@ impl RenderResourceSetBuilder {
 | 
			
		||||
        self.indexed_assignments.sort_by_key(|i| i.index);
 | 
			
		||||
        RenderResourceSet {
 | 
			
		||||
            id: RenderResourceSetId(self.hasher.finish()),
 | 
			
		||||
            indexed_assignments: self.indexed_assignments,
 | 
			
		||||
            indexed_assignments: Arc::new(self.indexed_assignments),
 | 
			
		||||
            dynamic_uniform_indices: if self.dynamic_uniform_indices.is_empty() {
 | 
			
		||||
                None
 | 
			
		||||
            } else {
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ use bevy_render::{
 | 
			
		||||
    base_render_graph,
 | 
			
		||||
    pipeline::{state_descriptors::*, PipelineDescriptor},
 | 
			
		||||
    render_graph::{
 | 
			
		||||
        nodes::{AssetUniformNode, UniformNode},
 | 
			
		||||
        nodes::{AssetRenderResourcesNode, RenderResourcesNode},
 | 
			
		||||
        RenderGraph,
 | 
			
		||||
    },
 | 
			
		||||
    shader::{Shader, ShaderStage, ShaderStages},
 | 
			
		||||
@ -123,23 +123,23 @@ impl SpriteRenderGraphBuilder for RenderGraph {
 | 
			
		||||
    fn add_sprite_graph(&mut self, resources: &Resources) -> &mut Self {
 | 
			
		||||
        self.add_system_node(
 | 
			
		||||
            node::COLOR_MATERIAL,
 | 
			
		||||
            AssetUniformNode::<ColorMaterial>::new(false),
 | 
			
		||||
            AssetRenderResourcesNode::<ColorMaterial>::new(false),
 | 
			
		||||
        );
 | 
			
		||||
        self.add_node_edge(node::COLOR_MATERIAL, base_render_graph::node::MAIN_PASS)
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        self.add_system_node(node::QUAD, UniformNode::<Quad>::new(false));
 | 
			
		||||
        self.add_system_node(node::QUAD, RenderResourcesNode::<Quad>::new(false));
 | 
			
		||||
        self.add_node_edge(node::QUAD, base_render_graph::node::MAIN_PASS)
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        self.add_system_node(
 | 
			
		||||
            node::SPRITE_SHEET,
 | 
			
		||||
            AssetUniformNode::<TextureAtlas>::new(false),
 | 
			
		||||
            AssetRenderResourcesNode::<TextureAtlas>::new(false),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        self.add_system_node(
 | 
			
		||||
            node::SPRITE_SHEET_SPRITE,
 | 
			
		||||
            UniformNode::<TextureAtlasSprite>::new(true),
 | 
			
		||||
            RenderResourcesNode::<TextureAtlasSprite>::new(true),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let mut pipelines = resources.get_mut::<Assets<PipelineDescriptor>>().unwrap();
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ fn setup(
 | 
			
		||||
            fragment: Some(shaders.add(Shader::from_glsl(ShaderStage::Fragment, FRAGMENT_SHADER))),
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        render_graph.add_system_node("my_material", AssetUniformNode::<MyMaterial>::new(true));
 | 
			
		||||
        render_graph.add_system_node("my_material", AssetRenderResourcesNode::<MyMaterial>::new(true));
 | 
			
		||||
        pipeline_handle
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,7 @@ fn setup(
 | 
			
		||||
            vertex: shaders.add(Shader::from_glsl(ShaderStage::Vertex, VERTEX_SHADER)),
 | 
			
		||||
            fragment: Some(shaders.add(Shader::from_glsl(ShaderStage::Fragment, FRAGMENT_SHADER))),
 | 
			
		||||
        }));
 | 
			
		||||
        render_graph.add_system_node("my_material", AssetUniformNode::<MyMaterial>::new(true));
 | 
			
		||||
        render_graph.add_system_node("my_material", AssetRenderResourcesNode::<MyMaterial>::new(true));
 | 
			
		||||
        pipeline_handle
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ pub use crate::{
 | 
			
		||||
        pipeline::PipelineDescriptor,
 | 
			
		||||
        render_graph::{
 | 
			
		||||
            nodes::{
 | 
			
		||||
                AssetUniformNode, CameraNode, MainPassNode, UniformNode, WindowSwapChainNode,
 | 
			
		||||
                AssetRenderResourcesNode, CameraNode, MainPassNode, RenderResourcesNode, WindowSwapChainNode,
 | 
			
		||||
                WindowTextureNode,
 | 
			
		||||
            },
 | 
			
		||||
            RenderGraph,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user