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.
This commit is contained in:
Joshua Yanovski 2021-04-10 16:53:34 +02:00
parent 12c3ee440e
commit 1bdf3b13a8
9 changed files with 298 additions and 152 deletions

View File

@ -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

View File

@ -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<String>,
pub clock: Clock,

View File

@ -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,
)),

View File

@ -13,6 +13,7 @@ use common::{
figure::Cell,
vol::{BaseVol, ReadVol, SizedVol, Vox},
};
use core::convert::TryFrom;
use vek::*;
type SpriteVertex = <SpritePipeline as render::Pipeline>::Vertex;
@ -79,10 +80,10 @@ where
};
let get_glow = |_vol: &mut V, _pos: Vec3<i32>| 0.0;
let get_opacity =
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true);
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_empty());
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, 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<Cell>, 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_::<usize>();
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<i32>| {
if vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true) {
let get_light = move |flat: &mut _, pos: Vec3<i32>| {
if flat_get(flat, pos).is_empty() {
1.0
} else {
0.0
}
};
let get_glow = |_vol: &mut V, _pos: Vec3<i32>| 0.0;
let get_color = |vol: &mut V, pos: Vec3<i32>| {
vol.get(pos)
.ok()
.and_then(|vox| vox.get_color())
.unwrap_or(Rgb::zero())
let get_glow = |_flat: &mut _, _pos: Vec3<i32>| 0.0;
let get_color = move |flat: &mut _, pos: Vec3<i32>| {
flat_get(flat, pos).get_color().unwrap_or(Rgb::zero())
};
let get_opacity =
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true);
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, 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<i32>| flat_get(flat, pos).is_empty();
let should_draw = move |flat: &mut _, pos: Vec3<i32>, delta: Vec3<i32>, 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_::<f32>();
let create_opaque = |atlas_pos, pos: Vec3<f32>, norm, _meta| {
SpriteVertex::new(atlas_pos, pos + mesh_delta, norm)
};
let create_opaque =
|atlas_pos, pos: Vec3<f32>, 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<i32>| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true);
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_empty());
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, 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<f32>, norm| ParticleVertex::new(pos, norm);

View File

@ -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<Texture<ColLightFmt>, 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,

View File

@ -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)?;

View File

@ -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<F: Float + FloatConst>(
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(

View File

@ -304,21 +304,37 @@ impl TerrainChunkData {
pub fn can_shadow_sun(&self) -> bool { self.visible.is_visible() || self.can_shadow_sun }
}
impl<V: RectRasterableVol> Terrain<V> {
#[derive(Clone)]
pub struct SpriteRenderContext {
sprite_config: Arc<SpriteSpec>,
sprite_data: Arc<HashMap<(SpriteKind, usize), Vec<SpriteData>>>,
sprite_col_lights: Texture<ColLightFmt>,
}
pub type SpriteRenderContextLazy = Box<dyn FnMut(&mut Renderer) -> SpriteRenderContext>;
impl SpriteRenderContext {
#[allow(clippy::float_cmp)] // TODO: Pending review in #587
pub fn new(renderer: &mut Renderer) -> Self {
pub fn new(renderer: &mut Renderer) -> SpriteRenderContextLazy {
let max_texture_size = renderer.max_texture_size();
struct SpriteDataResponse {
locals: [SpriteLocals; 8],
model: Mesh<SpritePipeline>,
offset: Vec3<f32>,
}
struct SpriteWorkerResponse {
sprite_config: Arc<SpriteSpec>,
sprite_data: HashMap<(SpriteKind, usize), Vec<SpriteDataResponse>>,
sprite_col_lights: ColLightInfo,
}
let join_handle = std::thread::spawn(move || {
// Load all the sprite config data.
let sprite_config =
Arc::<SpriteSpec>::load_expect("voxygen.voxel.sprite_manifest").cloned();
// Create a new mpsc (Multiple Produced, Single Consumer) pair for communicating
// with worker threads that are meshing chunks.
let (send, recv) = channel::unbounded();
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);
@ -367,7 +383,7 @@ impl<V: RectRasterableVol> Terrain<V> {
});
let sprite_mat: Mat4<f32> =
Mat4::translation_3d(offset).scaled_3d(SPRITE_SCALE);
move |greedy: &mut GreedyMesh, renderer: &mut Renderer| {
move |greedy: &mut GreedyMesh| {
(
(kind, variation),
scaled
@ -389,9 +405,6 @@ impl<V: RectRasterableVol> Terrain<V> {
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<f32> =
@ -409,12 +422,10 @@ impl<V: RectRasterableVol> Terrain<V> {
},
);
SpriteData {
/* vertex_range */ model,
SpriteDataResponse {
model: opaque_mesh,
offset,
locals: renderer.create_consts(&locals_buffer).expect(
"Failed to upload sprite locals to the GPU!",
),
locals: locals_buffer,
}
})
.collect::<Vec<_>>(),
@ -423,23 +434,99 @@ impl<V: RectRasterableVol> Terrain<V> {
},
)
})
.map(|mut f| f(&mut greedy, renderer))
.map(|mut f| f(&mut greedy))
.collect();
let sprite_col_lights = ShadowPipeline::create_col_lights(renderer, greedy.finalize())
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<V: RectRasterableVol> Terrain<V> {
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();
let (atlas, col_lights) =
Self::make_atlas(renderer).expect("Failed to create atlas texture");
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,

View File

@ -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,
);