From ac23b9ee36afeb24db9cbdff1f6f39bd10dd9514 Mon Sep 17 00:00:00 2001 From: AnthonyTornetta Date: Sun, 10 Sep 2023 02:09:33 -0400 Subject: [PATCH] Async-ified chunk rendering --- .../src/rendering/structure_renderer.rs | 465 +++++++++--------- cosmos_core/src/structure/block_health/mod.rs | 2 +- cosmos_core/src/structure/chunk.rs | 2 +- 3 files changed, 246 insertions(+), 223 deletions(-) diff --git a/cosmos_client/src/rendering/structure_renderer.rs b/cosmos_client/src/rendering/structure_renderer.rs index 17e8424ee..20f2a9abf 100644 --- a/cosmos_client/src/rendering/structure_renderer.rs +++ b/cosmos_client/src/rendering/structure_renderer.rs @@ -4,33 +4,34 @@ use crate::netty::flags::LocalPlayer; use crate::state::game_state::GameState; use crate::structure::planet::unload_chunks_far_from_players; use bevy::prelude::{ - in_state, warn, App, BuildChildren, Component, DespawnRecursiveExt, EventReader, GlobalTransform, IntoSystemConfigs, Mesh, PbrBundle, - PointLight, PointLightBundle, Quat, Rect, StandardMaterial, Transform, Update, Vec3, With, + in_state, warn, App, BuildChildren, Component, Deref, DerefMut, DespawnRecursiveExt, EventReader, GlobalTransform, IntoSystemConfigs, + Mesh, PbrBundle, PointLight, PointLightBundle, Quat, Rect, Resource, StandardMaterial, Transform, Update, Vec3, With, }; use bevy::reflect::Reflect; use bevy::render::primitives::Aabb; +use bevy::tasks::{AsyncComputeTaskPool, Task}; use bevy::utils::hashbrown::HashMap; use cosmos_core::block::{Block, BlockFace}; use cosmos_core::events::block_events::BlockChangedEvent; use cosmos_core::physics::location::SECTOR_DIMENSIONS; use cosmos_core::registry::identifiable::Identifiable; -use cosmos_core::registry::many_to_one::ManyToOneRegistry; -use cosmos_core::registry::Registry; +use cosmos_core::registry::many_to_one::{ManyToOneRegistry, ReadOnlyManyToOneRegistry}; +use cosmos_core::registry::{ReadOnlyRegistry, Registry}; use cosmos_core::structure::block_storage::BlockStorer; use cosmos_core::structure::chunk::{Chunk, ChunkEntity, CHUNK_DIMENSIONS, CHUNK_DIMENSIONSF}; use cosmos_core::structure::coordinates::{ChunkBlockCoordinate, ChunkCoordinate, UnboundChunkCoordinate}; use cosmos_core::structure::events::ChunkSetEvent; use cosmos_core::structure::Structure; use cosmos_core::utils::array_utils::expand; -use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; +use futures_lite::future; use std::collections::HashSet; use std::f32::consts::PI; -use std::sync::Mutex; +use std::mem::swap; -use crate::asset::asset_loading::{BlockTextureIndex, MainAtlas}; +use crate::asset::asset_loading::{BlockTextureIndex, MainAtlas, ReadOnlyMainAtlas}; use crate::{Assets, Commands, Entity, Handle, Query, Res, ResMut}; -use super::{BlockMeshRegistry, CosmosMeshBuilder, MeshBuilder, MeshInformation}; +use super::{BlockMeshRegistry, CosmosMeshBuilder, MeshBuilder, MeshInformation, ReadOnlyBlockMeshRegistry}; #[derive(Debug)] struct MeshMaterial { @@ -160,265 +161,285 @@ struct LightsHolder { #[derive(Component, Debug, Reflect, Default)] struct ChunkMeshes(Vec); -/// Performance hot spot -fn monitor_needs_rendered_system( +#[derive(Debug)] +struct ChunkRenderResult { + chunk_entity: Entity, + mesh: ChunkMesh, +} + +#[derive(Debug)] +struct RenderingChunk(Task); + +#[derive(Resource, Debug, DerefMut, Deref, Default)] +struct RenderingChunks(Vec); + +fn poll_rendering_chunks( + mut rendering_chunks: ResMut, mut commands: Commands, - structure_query: Query<&Structure>, - atlas: Res, mesh_query: Query>>, mut meshes: ResMut>, - blocks: Res>, - materials: Res>, - meshes_registry: Res, - lighting: Res>, lights_query: Query<&LightsHolder>, chunk_meshes_query: Query<&ChunkMeshes>, - block_textures: Res>, - - local_player: Query<&GlobalTransform, With>, - - chunks_need_rendered: Query<(Entity, &ChunkEntity, &GlobalTransform), With>, ) { - let Ok(local_transform) = local_player.get_single() else { - return; - }; - - // by making the Vec an Option I can take ownership of it later, which I cannot do with - // just a plain Mutex. - // https://stackoverflow.com/questions/30573188/cannot-move-data-out-of-a-mutex - let to_process = Mutex::new(Some(Vec::new())); - - let mut todo = chunks_need_rendered - .iter() - .map(|(x, y, transform)| (x, y, transform.translation().distance_squared(local_transform.translation()))) - // Only render chunks that are within a reasonable viewing distance - .filter(|(_, _, distance_sqrd)| *distance_sqrd < SECTOR_DIMENSIONS * SECTOR_DIMENSIONS) - .collect::>(); - - let chunks_per_frame = 10; + let mut todo = Vec::with_capacity(rendering_chunks.capacity()); - // Only sort first `chunks_per_frame`, so no built-in sort algorithm - let n: usize = chunks_per_frame.min(todo.len()); + swap(&mut rendering_chunks.0, &mut todo); - for i in 0..n { - let mut min = todo[i].2; - let mut best_i = i; + for mut rendering_chunk in todo { + if let Some(rendered_chunk) = future::block_on(future::poll_once(&mut rendering_chunk.0)) { + let (entity, mut chunk_mesh) = (rendered_chunk.chunk_entity, rendered_chunk.mesh); - for (j, item) in todo.iter().enumerate().skip(i + 1) { - if item.2 < min { - min = item.2; - best_i = j; + if commands.get_entity(entity).is_none() { + // Chunk may have been despawned during its rendering + continue; } - } - - todo.swap(i, best_i); - } - - // Render chunks in parallel - todo.par_iter().take(chunks_per_frame).copied().for_each(|(entity, ce, _)| { - let Ok(structure) = structure_query.get(ce.structure_entity) else { - return; - }; - - let mut renderer = ChunkRenderer::new(); - - let coords: ChunkCoordinate = ce.chunk_location; - - let Some(chunk) = structure.chunk_from_chunk_coordinates(coords) else { - return; - }; - let unbound: UnboundChunkCoordinate = coords.into(); - - let left = structure.chunk_from_chunk_coordinates_unbound(unbound.left()); - let right = structure.chunk_from_chunk_coordinates_unbound(unbound.right()); - let bottom = structure.chunk_from_chunk_coordinates_unbound(unbound.bottom()); - let top = structure.chunk_from_chunk_coordinates_unbound(unbound.top()); - let back = structure.chunk_from_chunk_coordinates_unbound(unbound.back()); - let front = structure.chunk_from_chunk_coordinates_unbound(unbound.front()); - - renderer.render( - &atlas, - &materials, - &lighting, - chunk, - left, - right, - bottom, - top, - back, - front, - &blocks, - &meshes_registry, - &block_textures, - ); - - let mut mutex = to_process.lock().expect("Error locking to_process vec!"); - - mutex.as_mut().unwrap().push((entity, renderer.create_mesh())); - }); - - let to_process_chunks = to_process.lock().unwrap().take().unwrap(); - - for (entity, mut chunk_mesh) in to_process_chunks { - commands.entity(entity).remove::(); + let mut old_mesh_entities = Vec::new(); - let mut old_mesh_entities = Vec::new(); + if let Ok(chunk_meshes_component) = chunk_meshes_query.get(entity) { + for ent in chunk_meshes_component.0.iter() { + let old_mesh_handle = mesh_query.get(*ent).expect("This should have a mesh component."); - if let Ok(chunk_meshes_component) = chunk_meshes_query.get(entity) { - for ent in chunk_meshes_component.0.iter() { - let old_mesh_handle = mesh_query.get(*ent).expect("This should have a mesh component."); + if let Some(old_mesh_handle) = old_mesh_handle { + meshes.remove(old_mesh_handle); + } - if let Some(old_mesh_handle) = old_mesh_handle { - meshes.remove(old_mesh_handle); + old_mesh_entities.push(*ent); } - - old_mesh_entities.push(*ent); } - } - let mut new_lights = LightsHolder::default(); + let mut new_lights = LightsHolder::default(); - if let Ok(lights) = lights_query.get(entity) { - for light in lights.lights.iter() { - let mut light = *light; - light.valid = false; - new_lights.lights.push(light); + if let Ok(lights) = lights_query.get(entity) { + for light in lights.lights.iter() { + let mut light = *light; + light.valid = false; + new_lights.lights.push(light); + } } - } - let mut entities_to_add = Vec::new(); - - if !chunk_mesh.lights.is_empty() { - for light in chunk_mesh.lights { - let (block_light_coord, properties) = light; - - let mut found = false; - for light in new_lights.lights.iter_mut() { - if light.position.x == block_light_coord.x - && light.position.y == block_light_coord.y - && light.position.z == block_light_coord.z - { - if light.light == properties { - light.valid = true; - found = true; + let mut entities_to_add = Vec::new(); + + if !chunk_mesh.lights.is_empty() { + for light in chunk_mesh.lights { + let (block_light_coord, properties) = light; + + let mut found = false; + for light in new_lights.lights.iter_mut() { + if light.position.x == block_light_coord.x + && light.position.y == block_light_coord.y + && light.position.z == block_light_coord.z + { + if light.light == properties { + light.valid = true; + found = true; + } + break; } - break; } - } - if !found { - let light_entity = commands - .spawn(PointLightBundle { - point_light: PointLight { - color: properties.color, - intensity: properties.intensity, - range: properties.range, - radius: 1.0, - // Shadows kill all performance - shadows_enabled: false, // !properties.shadows_disabled, + if !found { + let light_entity = commands + .spawn(PointLightBundle { + point_light: PointLight { + color: properties.color, + intensity: properties.intensity, + range: properties.range, + radius: 1.0, + // Shadows kill all performance + shadows_enabled: false, // !properties.shadows_disabled, + ..Default::default() + }, + transform: Transform::from_xyz( + block_light_coord.x as f32 - (CHUNK_DIMENSIONS as f32 / 2.0 - 0.5), + block_light_coord.y as f32 - (CHUNK_DIMENSIONS as f32 / 2.0 - 0.5), + block_light_coord.z as f32 - (CHUNK_DIMENSIONS as f32 / 2.0 - 0.5), + ), ..Default::default() - }, - transform: Transform::from_xyz( - block_light_coord.x as f32 - (CHUNK_DIMENSIONS as f32 / 2.0 - 0.5), - block_light_coord.y as f32 - (CHUNK_DIMENSIONS as f32 / 2.0 - 0.5), - block_light_coord.z as f32 - (CHUNK_DIMENSIONS as f32 / 2.0 - 0.5), - ), - ..Default::default() - }) - .id(); - - new_lights.lights.push(LightEntry { - entity: light_entity, - light: properties, - position: block_light_coord, - valid: true, - }); - entities_to_add.push(light_entity); + }) + .id(); + + new_lights.lights.push(LightEntry { + entity: light_entity, + light: properties, + position: block_light_coord, + valid: true, + }); + entities_to_add.push(light_entity); + } } } - } - for light in new_lights.lights.iter().filter(|x| !x.valid) { - commands.entity(light.entity).despawn_recursive(); - } + for light in new_lights.lights.iter().filter(|x| !x.valid) { + commands.entity(light.entity).despawn_recursive(); + } - new_lights.lights.retain(|x| x.valid); + new_lights.lights.retain(|x| x.valid); - // end lighting - // meshes + // end lighting + // meshes - // If the chunk previously only had one chunk mesh, then it would be on - // the chunk entity instead of child entities - commands - .entity(entity) - .remove::>() - .remove::>(); + // If the chunk previously only had one chunk mesh, then it would be on + // the chunk entity instead of child entities + commands + .entity(entity) + .remove::>() + .remove::>(); - let mut chunk_meshes_component = ChunkMeshes::default(); + let mut chunk_meshes_component = ChunkMeshes::default(); - if chunk_mesh.mesh_materials.len() > 1 { - for mesh_material in chunk_mesh.mesh_materials { - let mesh = meshes.add(mesh_material.mesh); + if chunk_mesh.mesh_materials.len() > 1 { + for mesh_material in chunk_mesh.mesh_materials { + let mesh = meshes.add(mesh_material.mesh); - let ent = if let Some(ent) = old_mesh_entities.pop() { - commands.entity(ent).insert(mesh).insert(mesh_material.material); + let ent = if let Some(ent) = old_mesh_entities.pop() { + commands.entity(ent).insert(mesh).insert(mesh_material.material); - ent - } else { - let s = (CHUNK_DIMENSIONS / 2) as f32; + ent + } else { + let s = (CHUNK_DIMENSIONS / 2) as f32; - let ent = commands - .spawn(( - PbrBundle { - mesh, - material: mesh_material.material, - ..Default::default() - }, - // Remove this once https://github.com/bevyengine/bevy/issues/4294 is done (bevy 0.12 released) - Aabb::from_min_max(Vec3::new(-s, -s, -s), Vec3::new(s, s, s)), - )) - .id(); + let ent = commands + .spawn(( + PbrBundle { + mesh, + material: mesh_material.material, + ..Default::default() + }, + // Remove this once https://github.com/bevyengine/bevy/issues/4294 is done (bevy 0.12 released) + Aabb::from_min_max(Vec3::new(-s, -s, -s), Vec3::new(s, s, s)), + )) + .id(); - entities_to_add.push(ent); + entities_to_add.push(ent); - ent - }; + ent + }; + + chunk_meshes_component.0.push(ent); + } + } else if !chunk_mesh.mesh_materials.is_empty() { + // To avoid making too many entities (and tanking performance), if only one mesh + // is present, just stick the mesh info onto the chunk itself. + + let mesh_material = chunk_mesh.mesh_materials.pop().expect("This has one element in it"); - chunk_meshes_component.0.push(ent); + let mesh = meshes.add(mesh_material.mesh); + let s = (CHUNK_DIMENSIONS / 2) as f32; + + commands.entity(entity).insert(( + mesh, + mesh_material.material, + // Remove this once https://github.com/bevyengine/bevy/issues/4294 is done (bevy 0.12 released) + Aabb::from_min_max(Vec3::new(-s, -s, -s), Vec3::new(s, s, s)), + )); } - } else if !chunk_mesh.mesh_materials.is_empty() { - // To avoid making too many entities (and tanking performance), if only one mesh - // is present, just stick the mesh info onto the chunk itself. - let mesh_material = chunk_mesh.mesh_materials.pop().expect("This has one element in it"); + // Any leftover entities are useless now, so kill them + for mesh in old_mesh_entities { + commands.entity(mesh).despawn_recursive(); + } - let mesh = meshes.add(mesh_material.mesh); - let s = (CHUNK_DIMENSIONS / 2) as f32; + let mut entity_commands = commands.entity(entity); - commands.entity(entity).insert(( - mesh, - mesh_material.material, - // Remove this once https://github.com/bevyengine/bevy/issues/4294 is done (bevy 0.12 released) - Aabb::from_min_max(Vec3::new(-s, -s, -s), Vec3::new(s, s, s)), - )); - } + for ent in entities_to_add { + entity_commands.add_child(ent); + } - // Any leftover entities are useless now, so kill them - for mesh in old_mesh_entities { - commands.entity(mesh).despawn_recursive(); + entity_commands + // .insert(meshes.add(chunk_mesh.mesh)) + .insert(new_lights) + .insert(chunk_meshes_component); + } else { + rendering_chunks.push(rendering_chunk); } + } +} - let mut entity_commands = commands.entity(entity); +/// Performance hot spot +fn monitor_needs_rendered_system( + mut commands: Commands, + structure_query: Query<&Structure>, + atlas: Res, + blocks: Res>, + materials: Res>, + meshes_registry: Res, + lighting: Res>, + block_textures: Res>, + mut rendering_chunks: ResMut, + local_player: Query<&GlobalTransform, With>, + chunks_need_rendered: Query<(Entity, &ChunkEntity, &GlobalTransform), With>, +) { + let Ok(local_transform) = local_player.get_single() else { + return; + }; - for ent in entities_to_add { - entity_commands.add_child(ent); - } + // by making the Vec an Option I can take ownership of it later, which I cannot do with + // just a plain Mutex. + // https://stackoverflow.com/questions/30573188/cannot-move-data-out-of-a-mutex + + for (entity, ce, _) in chunks_need_rendered + .iter() + .map(|(x, y, transform)| (x, y, transform.translation().distance_squared(local_transform.translation()))) + // Only render chunks that are within a reasonable viewing distance + .filter(|(_, _, distance_sqrd)| *distance_sqrd < SECTOR_DIMENSIONS * SECTOR_DIMENSIONS) + { + let async_task_pool = AsyncComputeTaskPool::get(); - entity_commands - // .insert(meshes.add(chunk_mesh.mesh)) - .insert(new_lights) - .insert(chunk_meshes_component); + let Ok(structure) = structure_query.get(ce.structure_entity) else { + continue; + }; + + let coords = ce.chunk_location; + + let Some(chunk) = structure.chunk_from_chunk_coordinates(coords).cloned() else { + continue; + }; + + let unbound = UnboundChunkCoordinate::from(coords); + + let left = structure.chunk_from_chunk_coordinates_unbound(unbound.left()).cloned(); + let right = structure.chunk_from_chunk_coordinates_unbound(unbound.right()).cloned(); + let bottom = structure.chunk_from_chunk_coordinates_unbound(unbound.bottom()).cloned(); + let top = structure.chunk_from_chunk_coordinates_unbound(unbound.top()).cloned(); + let back = structure.chunk_from_chunk_coordinates_unbound(unbound.back()).cloned(); + let front = structure.chunk_from_chunk_coordinates_unbound(unbound.front()).cloned(); + + let atlas = atlas.clone(); + let materials = materials.clone(); + let blocks = blocks.clone(); + let meshes_registry = meshes_registry.clone(); + let block_textures = block_textures.clone(); + let lighting = lighting.clone(); + + let task = async_task_pool.spawn(async move { + let mut renderer = ChunkRenderer::new(); + + renderer.render( + &atlas.atlas(), + &materials.registry(), + &lighting.registry(), + &chunk, + left.as_ref(), + right.as_ref(), + bottom.as_ref(), + top.as_ref(), + back.as_ref(), + front.as_ref(), + &blocks.registry(), + &meshes_registry.registry(), + &block_textures.registry(), + ); + + ChunkRenderResult { + chunk_entity: entity, + mesh: renderer.create_mesh(), + } + }); + + rendering_chunks.push(RenderingChunk(task)); + + commands.entity(entity).remove::(); } } @@ -706,10 +727,12 @@ impl ChunkRenderer { pub(super) fn register(app: &mut App) { app.add_systems( Update, - (monitor_needs_rendered_system, monitor_block_updates_system) + (monitor_block_updates_system, monitor_needs_rendered_system, poll_rendering_chunks) + .chain() .run_if(in_state(GameState::Playing)) .before(unload_chunks_far_from_players), ) // .add_system(add_renderer) + .init_resource::() .register_type::(); } diff --git a/cosmos_core/src/structure/block_health/mod.rs b/cosmos_core/src/structure/block_health/mod.rs index 433e0ad2f..79db8ef7c 100644 --- a/cosmos_core/src/structure/block_health/mod.rs +++ b/cosmos_core/src/structure/block_health/mod.rs @@ -12,7 +12,7 @@ use super::{ coordinates::{ChunkBlockCoordinate, Coordinate}, }; -#[derive(Debug, Default, Serialize, Deserialize, Reflect)] +#[derive(Debug, Default, Serialize, Deserialize, Reflect, Clone)] /// Each block's health is represented here pub struct BlockHealth { /// Block index -> block health diff --git a/cosmos_core/src/structure/chunk.rs b/cosmos_core/src/structure/chunk.rs index 083adec46..6a2cf3816 100644 --- a/cosmos_core/src/structure/chunk.rs +++ b/cosmos_core/src/structure/chunk.rs @@ -25,7 +25,7 @@ pub const CHUNK_DIMENSIONSF: f32 = CHUNK_DIMENSIONS as f32; /// Short for `CHUNK_DIMENSIONS as UnboundCoordinateType` pub const CHUNK_DIMENSIONS_UB: UnboundCoordinateType = CHUNK_DIMENSIONS as UnboundCoordinateType; -#[derive(Debug, Reflect, Serialize, Deserialize)] +#[derive(Debug, Reflect, Serialize, Deserialize, Clone)] /// Stores a bunch of blocks, information about those blocks, and where they are in the structure. pub struct Chunk { structure_position: ChunkCoordinate,