From 1bdf3b13a8e6d967ce5729f8597b222699d5af63 Mon Sep 17 00:00:00 2001 From: Joshua Yanovski Date: Sat, 10 Apr 2021 16:53:34 +0200 Subject: [PATCH] Mesh sprites in the background. This makes the delay afetr selecting a character before logging into the game much shorter, in the common case. It still doesn't handle things perfectly (it blocks creating Terrain::new if it's not finished, and it could be faster due to working in the background), but it's still a lot better than it was before. To improve sprite meshing performance, we also apply the terrain flat_get optimizations to sprites. Though I didn't initially know how much of an impact it would have, it feels significantly faster to me, though being able to parallelize it would be ideal. --- Cargo.toml | 1 + voxygen/src/lib.rs | 10 +- voxygen/src/main.rs | 6 +- voxygen/src/mesh/segment.rs | 87 +++++-- voxygen/src/render/pipelines/shadow.rs | 4 +- voxygen/src/scene/figure/mod.rs | 2 +- voxygen/src/scene/mod.rs | 12 +- voxygen/src/scene/terrain.rs | 327 ++++++++++++++++--------- voxygen/src/session.rs | 1 + 9 files changed, 298 insertions(+), 152 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2c7b8a0bbe..4b645c88fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ opt-level = 2 [profile.dev.package."veloren-server-cli"] opt-level = 2 [profile.dev.package."veloren-voxygen"] +incremental = true opt-level = 2 [profile.dev.package."veloren-world"] opt-level = 2 diff --git a/voxygen/src/lib.rs b/voxygen/src/lib.rs index 335ea8bb73..c5bfd4fe60 100644 --- a/voxygen/src/lib.rs +++ b/voxygen/src/lib.rs @@ -2,7 +2,14 @@ #![allow(incomplete_features)] #![allow(clippy::option_map_unit_fn)] #![deny(clippy::clone_on_ref_ptr)] -#![feature(array_map, bool_to_option, const_generics, drain_filter, or_patterns)] +#![feature( + array_map, + bool_to_option, + const_generics, + drain_filter, + once_cell, + or_patterns +)] #![recursion_limit = "2048"] #[macro_use] @@ -47,6 +54,7 @@ pub struct GlobalState { pub settings: Settings, pub profile: Profile, pub window: Window, + pub lazy_init: scene::terrain::SpriteRenderContextLazy, pub audio: AudioFrontend, pub info_message: Option, pub clock: Clock, diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index c3641e63da..764038d0a0 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -8,6 +8,7 @@ use veloren_voxygen::{ i18n::{self, i18n_asset_key, Localization}, profile::Profile, run, + scene::terrain::SpriteRenderContext, settings::{get_fps, AudioOutput, Settings}, window::Window, GlobalState, @@ -175,14 +176,17 @@ fn main() { i18n.read().log_missing_entries(); // Create window - let (window, event_loop) = Window::new(&settings).expect("Failed to create window!"); + let (mut window, event_loop) = Window::new(&settings).expect("Failed to create window!"); let clipboard = iced_winit::Clipboard::new(window.window()); + let lazy_init = SpriteRenderContext::new(window.renderer_mut()); + let global_state = GlobalState { audio, profile, window, + lazy_init, clock: Clock::new(std::time::Duration::from_secs_f64( 1.0 / get_fps(settings.graphics.max_fps) as f64, )), diff --git a/voxygen/src/mesh/segment.rs b/voxygen/src/mesh/segment.rs index 2ad0ffac24..eca4cc9d62 100644 --- a/voxygen/src/mesh/segment.rs +++ b/voxygen/src/mesh/segment.rs @@ -13,6 +13,7 @@ use common::{ figure::Cell, vol::{BaseVol, ReadVol, SizedVol, Vox}, }; +use core::convert::TryFrom; use vek::*; type SpriteVertex = ::Vertex; @@ -79,10 +80,10 @@ where }; let get_glow = |_vol: &mut V, _pos: Vec3| 0.0; let get_opacity = - |vol: &mut V, pos: Vec3| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true); + |vol: &mut V, pos: Vec3| vol.get(pos).map_or(true, |vox| vox.is_empty()); let should_draw = |vol: &mut V, pos: Vec3, delta: Vec3, uv| { should_draw_greedy(pos, delta, uv, |vox| { - vol.get(vox).map(|vox| *vox).unwrap_or(Vox::empty()) + vol.get(vox).map(|vox| *vox).unwrap_or(Cell::empty()) }) }; let create_opaque = |atlas_pos, pos, norm| { @@ -161,6 +162,14 @@ where && lower_bound.y <= upper_bound.y && lower_bound.z <= upper_bound.z ); + // Lower bound coordinates must fit in an i16 (which means upper bound + // coordinates fit as integers in a f23). + assert!( + i16::try_from(lower_bound.x).is_ok() + && i16::try_from(lower_bound.y).is_ok() + && i16::try_from(lower_bound.z).is_ok(), + "Sprite offsets should fit in i16", + ); let greedy_size = upper_bound - lower_bound + 1; // TODO: Should this be 16, 16, 64? assert!( @@ -168,39 +177,69 @@ where "Sprite size out of bounds: {:?} ≤ (31, 31, 63)", greedy_size - 1 ); + + let (flat, flat_get) = { + let (w, h, d) = (greedy_size + 2).into_tuple(); + let flat = { + let vol = self; + + let mut flat = vec![Cell::empty(); (w * h * d) as usize]; + let mut i = 0; + for x in -1..greedy_size.x + 1 { + for y in -1..greedy_size.y + 1 { + for z in -1..greedy_size.z + 1 { + let wpos = lower_bound + Vec3::new(x, y, z); + let block = vol.get(wpos).map(|b| *b).unwrap_or(Cell::empty()); + flat[i] = block; + i += 1; + } + } + } + flat + }; + + let flat_get = move |flat: &Vec, Vec3 { x, y, z }| match flat + .get((x * h * d + y * d + z) as usize) + .copied() + { + Some(b) => b, + None => panic!("x {} y {} z {} d {} h {}", x, y, z, d, h), + }; + + (flat, flat_get) + }; + // NOTE: Cast to usize is safe because of previous check, since all values fit // into u16 which is safe to cast to usize. let greedy_size = greedy_size.as_::(); let greedy_size_cross = greedy_size; - let draw_delta = lower_bound; + let draw_delta = Vec3::new(1, 1, 1); - let get_light = |vol: &mut V, pos: Vec3| { - if vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true) { + let get_light = move |flat: &mut _, pos: Vec3| { + if flat_get(flat, pos).is_empty() { 1.0 } else { 0.0 } }; - let get_glow = |_vol: &mut V, _pos: Vec3| 0.0; - let get_color = |vol: &mut V, pos: Vec3| { - vol.get(pos) - .ok() - .and_then(|vox| vox.get_color()) - .unwrap_or(Rgb::zero()) + let get_glow = |_flat: &mut _, _pos: Vec3| 0.0; + let get_color = move |flat: &mut _, pos: Vec3| { + flat_get(flat, pos).get_color().unwrap_or(Rgb::zero()) }; - let get_opacity = - |vol: &mut V, pos: Vec3| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true); - let should_draw = |vol: &mut V, pos: Vec3, delta: Vec3, uv| { - should_draw_greedy_ao(vertical_stripes, pos, delta, uv, |vox| { - vol.get(vox).map(|vox| *vox).unwrap_or(Vox::empty()) - }) + let get_opacity = move |flat: &mut _, pos: Vec3| flat_get(flat, pos).is_empty(); + let should_draw = move |flat: &mut _, pos: Vec3, delta: Vec3, uv| { + should_draw_greedy_ao(vertical_stripes, pos, delta, uv, |vox| flat_get(flat, vox)) + }; + // NOTE: Fits in i16 (much lower actually) so f32 is no problem (and the final + // position, pos + mesh_delta, is guaranteed to fit in an f32). + let mesh_delta = lower_bound.as_::(); + let create_opaque = |atlas_pos, pos: Vec3, norm, _meta| { + SpriteVertex::new(atlas_pos, pos + mesh_delta, norm) }; - let create_opaque = - |atlas_pos, pos: Vec3, norm, _meta| SpriteVertex::new(atlas_pos, pos, norm); greedy.push(GreedyConfig { - data: self, + data: flat, draw_delta, greedy_size, greedy_size_cross, @@ -219,8 +258,8 @@ where |atlas_pos, pos, norm, &meta| create_opaque(atlas_pos, pos, norm, meta), )); }, - make_face_texel: move |vol: &mut V, pos, light, glow| { - TerrainVertex::make_col_light(light, glow, get_color(vol, pos)) + make_face_texel: move |flat: &mut _, pos, light, glow| { + TerrainVertex::make_col_light(light, glow, get_color(flat, pos)) }, }); @@ -288,10 +327,10 @@ where .unwrap_or(Rgb::zero()) }; let get_opacity = - |vol: &mut V, pos: Vec3| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true); + |vol: &mut V, pos: Vec3| vol.get(pos).map_or(true, |vox| vox.is_empty()); let should_draw = |vol: &mut V, pos: Vec3, delta: Vec3, uv| { should_draw_greedy(pos, delta, uv, |vox| { - vol.get(vox).map(|vox| *vox).unwrap_or(Vox::empty()) + vol.get(vox).map(|vox| *vox).unwrap_or(Cell::empty()) }) }; let create_opaque = |_atlas_pos, pos: Vec3, norm| ParticleVertex::new(pos, norm); diff --git a/voxygen/src/render/pipelines/shadow.rs b/voxygen/src/render/pipelines/shadow.rs index cee7a2fd1e..1f1069b4b6 100644 --- a/voxygen/src/render/pipelines/shadow.rs +++ b/voxygen/src/render/pipelines/shadow.rs @@ -67,7 +67,7 @@ pub struct ShadowPipeline; impl ShadowPipeline { pub fn create_col_lights( renderer: &mut Renderer, - (col_lights, col_lights_size): ColLightInfo, + (col_lights, col_lights_size): &ColLightInfo, ) -> Result, RenderError> { renderer.create_texture_immutable_raw( gfx::texture::Kind::D2( @@ -76,7 +76,7 @@ impl ShadowPipeline { gfx::texture::AaMode::Single, ), gfx::texture::Mipmap::Provided, - &[&col_lights], + &[col_lights], gfx::texture::SamplerInfo::new( gfx::texture::FilterMethod::Bilinear, gfx::texture::WrapMode::Clamp, diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index c34009b23b..59fb55bfc8 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -4904,7 +4904,7 @@ impl FigureColLights { i32::from(tex_size.y), )) .expect("Not yet implemented: allocate new atlas on allocation failure."); - let col_lights = ShadowPipeline::create_col_lights(renderer, (tex, tex_size))?; + let col_lights = ShadowPipeline::create_col_lights(renderer, &(tex, tex_size))?; let model_len = u32::try_from(opaque.vertices().len()) .expect("The model size for this figure does not fit in a u32!"); let model = renderer.create_model(&opaque)?; diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 5da05fdfa0..b300b5ffeb 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -11,7 +11,7 @@ pub use self::{ figure::FigureMgr, lod::Lod, particle::ParticleMgr, - terrain::Terrain, + terrain::{SpriteRenderContextLazy, Terrain}, }; use crate::{ audio::{ambient::AmbientMgr, music::MusicMgr, sfx::SfxMgr, AudioFrontend}, @@ -267,8 +267,14 @@ fn compute_warping_parameter_perspective( impl Scene { /// Create a new `Scene` with default parameters. - pub fn new(renderer: &mut Renderer, client: &Client, settings: &Settings) -> Self { + pub fn new( + renderer: &mut Renderer, + lazy_init: &mut SpriteRenderContextLazy, + client: &Client, + settings: &Settings, + ) -> Self { let resolution = renderer.get_resolution().map(|e| e as f32); + let sprite_render_context = lazy_init(renderer); Self { data: GlobalModel { @@ -301,7 +307,7 @@ impl Scene { .create_consts(&[PostProcessLocals::default()]) .unwrap(), }, - terrain: Terrain::new(renderer), + terrain: Terrain::new(renderer, sprite_render_context), lod: Lod::new(renderer, client, settings), loaded_distance: 0.0, map_bounds: Vec2::new( diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index 85a1db5dce..3d67e4c060 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -304,13 +304,211 @@ impl TerrainChunkData { pub fn can_shadow_sun(&self) -> bool { self.visible.is_visible() || self.can_shadow_sun } } -impl Terrain { - #[allow(clippy::float_cmp)] // TODO: Pending review in #587 - pub fn new(renderer: &mut Renderer) -> Self { - // Load all the sprite config data. - let sprite_config = - Arc::::load_expect("voxygen.voxel.sprite_manifest").cloned(); +#[derive(Clone)] +pub struct SpriteRenderContext { + sprite_config: Arc, + sprite_data: Arc>>, + sprite_col_lights: Texture, +} +pub type SpriteRenderContextLazy = Box SpriteRenderContext>; + +impl SpriteRenderContext { + #[allow(clippy::float_cmp)] // TODO: Pending review in #587 + pub fn new(renderer: &mut Renderer) -> SpriteRenderContextLazy { + let max_texture_size = renderer.max_texture_size(); + + struct SpriteDataResponse { + locals: [SpriteLocals; 8], + model: Mesh, + offset: Vec3, + } + + struct SpriteWorkerResponse { + sprite_config: Arc, + sprite_data: HashMap<(SpriteKind, usize), Vec>, + sprite_col_lights: ColLightInfo, + } + + let join_handle = std::thread::spawn(move || { + // Load all the sprite config data. + let sprite_config = + Arc::::load_expect("voxygen.voxel.sprite_manifest").cloned(); + + let max_size = + guillotiere::Size::new(i32::from(max_texture_size), i32::from(max_texture_size)); + let mut greedy = GreedyMesh::new(max_size); + let mut locals_buffer = [SpriteLocals::default(); 8]; + let sprite_config_ = &sprite_config; + // NOTE: Tracks the start vertex of the next model to be meshed. + + let sprite_data: HashMap<(SpriteKind, usize), _> = SpriteKind::into_enum_iter() + .filter_map(|kind| Some((kind, kind.elim_case_pure(&sprite_config_.0).as_ref()?))) + .flat_map(|(kind, sprite_config)| { + let wind_sway = sprite_config.wind_sway; + sprite_config.variations.iter().enumerate().map( + move |( + variation, + SpriteModelConfig { + model, + offset, + lod_axes, + }, + )| { + let scaled = [1.0, 0.8, 0.6, 0.4, 0.2]; + let offset = Vec3::from(*offset); + let lod_axes = Vec3::from(*lod_axes); + let model = DotVoxAsset::load_expect(model); + let zero = Vec3::zero(); + let model_size = model + .read() + .0 + .models + .first() + .map( + |&dot_vox::Model { + size: dot_vox::Size { x, y, z }, + .. + }| Vec3::new(x, y, z), + ) + .unwrap_or(zero); + let max_model_size = Vec3::new(31.0, 31.0, 63.0); + let model_scale = max_model_size.map2(model_size, |max_sz: f32, cur_sz| { + let scale = max_sz / max_sz.max(cur_sz as f32); + if scale < 1.0 && (cur_sz as f32 * scale).ceil() > max_sz { + scale - 0.001 + } else { + scale + } + }); + let sprite_mat: Mat4 = + Mat4::translation_3d(offset).scaled_3d(SPRITE_SCALE); + move |greedy: &mut GreedyMesh| { + ( + (kind, variation), + scaled + .iter() + .map(|&lod_scale_orig| { + let lod_scale = model_scale + * if lod_scale_orig == 1.0 { + Vec3::broadcast(1.0) + } else { + lod_axes * lod_scale_orig + + lod_axes + .map(|e| if e == 0.0 { 1.0 } else { 0.0 }) + }; + // Mesh generation exclusively acts using side effects; it + // has no + // interesting return value, but updates the mesh. + let mut opaque_mesh = Mesh::new(); + Meshable::::generate_mesh( + Segment::from(&model.read().0).scaled_by(lod_scale), + (greedy, &mut opaque_mesh, false), + ); + + let sprite_scale = Vec3::one() / lod_scale; + let sprite_mat: Mat4 = + sprite_mat * Mat4::scaling_3d(sprite_scale); + locals_buffer.iter_mut().enumerate().for_each( + |(ori, locals)| { + let sprite_mat = sprite_mat + .rotated_z(f32::consts::PI * 0.25 * ori as f32); + *locals = SpriteLocals::new( + sprite_mat, + sprite_scale, + offset, + wind_sway, + ); + }, + ); + + SpriteDataResponse { + model: opaque_mesh, + offset, + locals: locals_buffer, + } + }) + .collect::>(), + ) + } + }, + ) + }) + .map(|mut f| f(&mut greedy)) + .collect(); + + let sprite_col_lights = greedy.finalize(); + + SpriteWorkerResponse { + sprite_config, + sprite_data, + sprite_col_lights, + } + }); + + let init = core::lazy::OnceCell::new(); + let mut join_handle = Some(join_handle); + let mut closure = move |renderer: &mut Renderer| { + // The second unwrap can only fail if the sprite meshing thread panics, which + // implies that our sprite assets either were not found or did not + // satisfy the size requirements for meshing, both of which are + // considered invariant violations. + let SpriteWorkerResponse { + sprite_config, + sprite_data, + sprite_col_lights, + } = join_handle + .take() + .expect( + "Closure should only be called once (in a `OnceCell::get_or_init`) in the \ + absence of caught panics!", + ) + .join() + .unwrap(); + + let sprite_data = sprite_data + .into_iter() + .map(|(key, models)| { + ( + key, + models + .into_iter() + .map( + |SpriteDataResponse { + locals, + model, + offset, + }| { + SpriteData { + locals: renderer + .create_consts(&locals) + .expect("Failed to upload sprite locals to the GPU!"), + model: renderer.create_model(&model).expect( + "Failed to upload sprite model data to the GPU!", + ), + offset, + } + }, + ) + .collect(), + ) + }) + .collect(); + let sprite_col_lights = ShadowPipeline::create_col_lights(renderer, &sprite_col_lights) + .expect("Failed to upload sprite color and light data to the GPU!"); + + Self { + sprite_config: Arc::clone(&sprite_config), + sprite_data: Arc::new(sprite_data), + sprite_col_lights, + } + }; + Box::new(move |renderer| init.get_or_init(|| closure(renderer)).clone()) + } +} + +impl Terrain { + pub fn new(renderer: &mut Renderer, sprite_render_context: SpriteRenderContext) -> Self { // Create a new mpsc (Multiple Produced, Single Consumer) pair for communicating // with worker threads that are meshing chunks. let (send, recv) = channel::unbounded(); @@ -318,128 +516,17 @@ impl Terrain { let (atlas, col_lights) = Self::make_atlas(renderer).expect("Failed to create atlas texture"); - let max_texture_size = renderer.max_texture_size(); - let max_size = - guillotiere::Size::new(i32::from(max_texture_size), i32::from(max_texture_size)); - let mut greedy = GreedyMesh::new(max_size); - let mut locals_buffer = [SpriteLocals::default(); 8]; - let sprite_config_ = &sprite_config; - // NOTE: Tracks the start vertex of the next model to be meshed. - - let sprite_data: HashMap<(SpriteKind, usize), _> = SpriteKind::into_enum_iter() - .filter_map(|kind| Some((kind, kind.elim_case_pure(&sprite_config_.0).as_ref()?))) - .flat_map(|(kind, sprite_config)| { - let wind_sway = sprite_config.wind_sway; - sprite_config.variations.iter().enumerate().map( - move |( - variation, - SpriteModelConfig { - model, - offset, - lod_axes, - }, - )| { - let scaled = [1.0, 0.8, 0.6, 0.4, 0.2]; - let offset = Vec3::from(*offset); - let lod_axes = Vec3::from(*lod_axes); - let model = DotVoxAsset::load_expect(model); - let zero = Vec3::zero(); - let model_size = model - .read() - .0 - .models - .first() - .map( - |&dot_vox::Model { - size: dot_vox::Size { x, y, z }, - .. - }| Vec3::new(x, y, z), - ) - .unwrap_or(zero); - let max_model_size = Vec3::new(31.0, 31.0, 63.0); - let model_scale = max_model_size.map2(model_size, |max_sz: f32, cur_sz| { - let scale = max_sz / max_sz.max(cur_sz as f32); - if scale < 1.0 && (cur_sz as f32 * scale).ceil() > max_sz { - scale - 0.001 - } else { - scale - } - }); - let sprite_mat: Mat4 = - Mat4::translation_3d(offset).scaled_3d(SPRITE_SCALE); - move |greedy: &mut GreedyMesh, renderer: &mut Renderer| { - ( - (kind, variation), - scaled - .iter() - .map(|&lod_scale_orig| { - let lod_scale = model_scale - * if lod_scale_orig == 1.0 { - Vec3::broadcast(1.0) - } else { - lod_axes * lod_scale_orig - + lod_axes - .map(|e| if e == 0.0 { 1.0 } else { 0.0 }) - }; - // Mesh generation exclusively acts using side effects; it - // has no - // interesting return value, but updates the mesh. - let mut opaque_mesh = Mesh::new(); - Meshable::::generate_mesh( - Segment::from(&model.read().0).scaled_by(lod_scale), - (greedy, &mut opaque_mesh, false), - ); - let model = renderer.create_model(&opaque_mesh).expect( - "Failed to upload sprite model data to the GPU!", - ); - - let sprite_scale = Vec3::one() / lod_scale; - let sprite_mat: Mat4 = - sprite_mat * Mat4::scaling_3d(sprite_scale); - locals_buffer.iter_mut().enumerate().for_each( - |(ori, locals)| { - let sprite_mat = sprite_mat - .rotated_z(f32::consts::PI * 0.25 * ori as f32); - *locals = SpriteLocals::new( - sprite_mat, - sprite_scale, - offset, - wind_sway, - ); - }, - ); - - SpriteData { - /* vertex_range */ model, - offset, - locals: renderer.create_consts(&locals_buffer).expect( - "Failed to upload sprite locals to the GPU!", - ), - } - }) - .collect::>(), - ) - } - }, - ) - }) - .map(|mut f| f(&mut greedy, renderer)) - .collect(); - - let sprite_col_lights = ShadowPipeline::create_col_lights(renderer, greedy.finalize()) - .expect("Failed to upload sprite color and light data to the GPU!"); - Self { atlas, - sprite_config, + sprite_config: sprite_render_context.sprite_config, chunks: HashMap::default(), shadow_chunks: Vec::default(), mesh_send_tmp: send, mesh_recv: recv, mesh_todo: HashMap::default(), mesh_todos_active: Arc::new(AtomicU64::new(0)), - sprite_data: Arc::new(sprite_data), - sprite_col_lights, + sprite_data: sprite_render_context.sprite_data, + sprite_col_lights: sprite_render_context.sprite_col_lights, waves: renderer .create_texture( &assets::Image::load_expect("voxygen.texture.waves").read().0, diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index e12d806ce3..ae9f822517 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -84,6 +84,7 @@ impl SessionState { // game world. let mut scene = Scene::new( global_state.window.renderer_mut(), + &mut global_state.lazy_init, &*client.borrow(), &global_state.settings, );