From 2c15d0af560445c95a94fe4a5eaa5d299e4ccc2d Mon Sep 17 00:00:00 2001 From: Joshua Yanovski Date: Fri, 9 Sep 2022 08:29:43 -0700 Subject: [PATCH] Reduce overhead of messaging systems. --- Cargo.lock | 29 +- client/src/bin/swarm/main.rs | 31 +- client/src/lib.rs | 655 +++++++++--------- common/frontend/Cargo.toml | 2 +- common/net/src/msg/world_msg.rs | 10 +- common/src/terrain/map.rs | 21 +- common/src/terrain/mod.rs | 10 + common/src/volumes/vol_grid_2d.rs | 64 +- common/state/src/state.rs | 53 +- common/systems/src/phys.rs | 6 +- common/systems/tests/character_state.rs | 20 +- common/systems/tests/phys/utils.rs | 20 +- network/benches/speed.rs | 6 +- network/examples/chat.rs | 26 +- network/examples/fileshare/server.rs | 64 +- network/examples/network-speed/main.rs | 6 +- network/src/api.rs | 121 ++-- network/src/lib.rs | 4 +- network/src/message.rs | 4 +- network/tests/closing.rs | 74 +- network/tests/helper.rs | 4 +- network/tests/integration.rs | 40 +- server/Cargo.toml | 2 + server/src/chunk_generator.rs | 13 + server/src/client.rs | 65 +- server/src/connection_handler.rs | 32 +- server/src/lib.rs | 81 ++- server/src/login_provider.rs | 24 +- server/src/settings.rs | 2 +- server/src/sys/chunk_serialize.rs | 2 +- server/src/sys/msg/character_screen.rs | 8 +- server/src/sys/msg/general.rs | 48 +- server/src/sys/msg/mod.rs | 2 +- server/src/sys/msg/ping.rs | 77 +- server/src/sys/msg/register.rs | 489 ++++++++----- server/src/sys/msg/terrain.rs | 16 +- server/src/sys/sentinel.rs | 20 +- server/src/sys/terrain.rs | 407 ++++++++--- server/src/sys/terrain_sync.rs | 87 ++- server/src/test_world.rs | 2 + voxygen/benches/meshing_benchmark.rs | 6 +- voxygen/src/scene/terrain.rs | 6 +- .../examples/chunk_compression_benchmarks.rs | 6 +- world/src/lib.rs | 13 +- world/src/sim/mod.rs | 9 +- 45 files changed, 1645 insertions(+), 1042 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 667075b0f2..5554c86209 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1752,6 +1752,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "drop_guard" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a817d8b683f6e649aed359aab0c47a875377516bb5791d0f7e46d9066d209" + [[package]] name = "egui" version = "0.12.0" @@ -4621,7 +4627,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f61dcf0b917cd75d4521d7343d1ffff3d1583054133c9b5cbea3375c703c40d" dependencies = [ "profiling-procmacros", - "tracy-client 0.13.2", + "tracy-client", ] [[package]] @@ -6387,13 +6393,13 @@ dependencies = [ [[package]] name = "tracing-tracy" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3ebef1f9f0d00aaa29239537effef65b82c56040c680f540fc6cedfac7b230" +checksum = "23a42311a35ed976d72f359de43e9fe028ec9d9f1051c4c52bd05a4f66ff3cbf" dependencies = [ "tracing-core", "tracing-subscriber", - "tracy-client 0.14.0", + "tracy-client", ] [[package]] @@ -6407,17 +6413,6 @@ dependencies = [ "tracy-client-sys", ] -[[package]] -name = "tracy-client" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f901ea566c34f5fdc987962495ebfea20c18d781e271967edcc0f9897e339815" -dependencies = [ - "loom", - "once_cell", - "tracy-client-sys", -] - [[package]] name = "tracy-client-sys" version = "0.17.1" @@ -6763,7 +6758,7 @@ version = "0.10.0" dependencies = [ "directories-next", "tracing", - "tracy-client 0.13.2", + "tracy-client", ] [[package]] @@ -6937,6 +6932,7 @@ dependencies = [ "chrono", "chrono-tz", "crossbeam-channel", + "drop_guard", "enumset", "futures-util", "hashbrown 0.12.3", @@ -6945,6 +6941,7 @@ dependencies = [ "lazy_static", "noise", "num_cpus", + "parking_lot 0.12.1", "portpicker", "prometheus", "prometheus-hyper", diff --git a/client/src/bin/swarm/main.rs b/client/src/bin/swarm/main.rs index 7284ddb95b..ae85810d01 100644 --- a/client/src/bin/swarm/main.rs +++ b/client/src/bin/swarm/main.rs @@ -30,7 +30,7 @@ struct Opt { fn main() { let opt = Opt::from_args(); // Start logging - common_frontend::init_stdout(None); + let _guards = common_frontend::init_stdout(None); // Run clients and stuff // // NOTE: "swarm0" is assumed to be an admin already @@ -72,9 +72,7 @@ fn main() { ); }); - loop { - thread::sleep(Duration::from_secs_f32(1.0)); - } + std::thread::park(); } fn run_client_new_thread( @@ -102,23 +100,26 @@ fn run_client( opt: Opt, finished_init: Arc, ) -> Result<(), veloren_client::Error> { - // Connect to localhost - let addr = ConnectionArgs::Tcp { - prefer_ipv6: false, - hostname: "localhost".into(), - }; - let runtime_clone = Arc::clone(&runtime); - // NOTE: use a no-auth server - let mut client = runtime - .block_on(Client::new( + let mut client = loop { + // Connect to localhost + let addr = ConnectionArgs::Tcp { + prefer_ipv6: false, + hostname: "localhost".into(), + }; + let runtime_clone = Arc::clone(&runtime); + // NOTE: use a no-auth server + match runtime.block_on(Client::new( addr, runtime_clone, &mut None, &username, "", |_| false, - )) - .expect("Failed to connect to the server"); + )) { + Err(e) => tracing::warn!(?e, "Client {} disconnected", index), + Ok(client) => break client, + } + }; let mut clock = common::clock::Clock::new(Duration::from_secs_f32(1.0 / 30.0)); diff --git a/client/src/lib.rs b/client/src/lib.rs index ffc8579b39..2c41087d7a 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -39,11 +39,11 @@ use common::{ mounting::Rider, outcome::Outcome, recipe::{ComponentRecipeBook, RecipeBook}, - resources::{PlayerEntity, TimeOfDay}, + resources::{GameMode, PlayerEntity, TimeOfDay}, spiral::Spiral2d, terrain::{ block::Block, map::MapConfig, neighbors, site::DungeonKindMeta, BiomeKind, SiteKindMeta, - SpriteKind, TerrainChunk, TerrainChunkSize, + SpriteKind, TerrainChunk, TerrainChunkSize, TerrainGrid, }, trade::{PendingTrade, SitePrices, TradeAction, TradeId, TradeResult}, uid::{Uid, UidAllocator}, @@ -281,7 +281,7 @@ impl Client { ) -> Result { let network = Network::new(Pid::new(), &runtime); - let participant = match addr { + let mut participant = match addr { ConnectionArgs::Tcp { hostname, prefer_ipv6, @@ -304,7 +304,7 @@ impl Client { }; let stream = participant.opened().await?; - let mut ping_stream = participant.opened().await?; + let ping_stream = participant.opened().await?; let mut register_stream = participant.opened().await?; let character_screen_stream = participant.opened().await?; let in_game_stream = participant.opened().await?; @@ -340,6 +340,314 @@ impl Client { // Wait for initial sync let mut ping_interval = tokio::time::interval(Duration::from_secs(1)); + let ServerInit::GameSync { + entity_package, + time_of_day, + max_group_size, + client_timeout, + world_map, + recipe_book, + component_recipe_book, + material_stats, + ability_map, + } = loop { + tokio::select! { + // Spawn in a blocking thread (leaving the network thread free). This is mostly + // useful for bots. + res = register_stream.recv() => break res?, + _ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?, + } + }; + + // Spawn in a blocking thread (leaving the network thread free). This is mostly + // useful for bots. + let mut task = tokio::task::spawn_blocking(move || { + let map_size_lg = + common::terrain::MapSizeLg::new(world_map.dimensions_lg).map_err(|_| { + Error::Other(format!( + "Server sent bad world map dimensions: {:?}", + world_map.dimensions_lg, + )) + })?; + let sea_level = world_map.default_chunk.get_min_z() as f32; + + // Initialize `State` + let pools = State::pools(GameMode::Client); + let mut state = State::client(pools, map_size_lg, world_map.default_chunk); + // Client-only components + state.ecs_mut().register::>(); + let entity = state.ecs_mut().apply_entity_package(entity_package); + *state.ecs_mut().write_resource() = time_of_day; + *state.ecs_mut().write_resource() = PlayerEntity(Some(entity)); + state.ecs_mut().insert(material_stats); + state.ecs_mut().insert(ability_map); + + let map_size = map_size_lg.chunks(); + let max_height = world_map.max_height; + let rgba = world_map.rgba; + let alt = world_map.alt; + if rgba.size() != map_size.map(|e| e as i32) { + return Err(Error::Other("Server sent a bad world map image".into())); + } + if alt.size() != map_size.map(|e| e as i32) { + return Err(Error::Other("Server sent a bad altitude map.".into())); + } + let [west, east] = world_map.horizons; + let scale_angle = |a: u8| (a as f32 / 255.0 * ::FRAC_PI_2()).tan(); + let scale_height = |h: u8| h as f32 / 255.0 * max_height; + let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height; + + debug!("Preparing image..."); + let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| { + ( + angles.iter().copied().map(scale_angle).collect::>(), + heights + .iter() + .copied() + .map(scale_height) + .collect::>(), + ) + }; + let horizons = [unzip_horizons(&west), unzip_horizons(&east)]; + + // Redraw map (with shadows this time). + let mut world_map_rgba = vec![0u32; rgba.size().product() as usize]; + let mut world_map_topo = vec![0u32; rgba.size().product() as usize]; + let mut map_config = common::terrain::map::MapConfig::orthographic( + map_size_lg, + core::ops::RangeInclusive::new(0.0, max_height), + ); + map_config.horizons = Some(&horizons); + let rescale_height = |h: f32| h / max_height; + let bounds_check = |pos: Vec2| { + pos.reduce_partial_min() >= 0 + && pos.x < map_size.x as i32 + && pos.y < map_size.y as i32 + }; + fn sample_pos( + map_config: &MapConfig, + pos: Vec2, + alt: &Grid, + rgba: &Grid, + map_size: &Vec2, + map_size_lg: &common::terrain::MapSizeLg, + max_height: f32, + ) -> common::terrain::map::MapSample { + let rescale_height = |h: f32| h / max_height; + let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height; + let bounds_check = |pos: Vec2| { + pos.reduce_partial_min() >= 0 + && pos.x < map_size.x as i32 + && pos.y < map_size.y as i32 + }; + let MapConfig { + gain, + is_contours, + is_height_map, + is_stylized_topo, + .. + } = *map_config; + let mut is_contour_line = false; + let mut is_border = false; + let (rgb, alt, downhill_wpos) = if bounds_check(pos) { + let posi = pos.y as usize * map_size.x as usize + pos.x as usize; + let [r, g, b, _a] = rgba[pos].to_le_bytes(); + let is_water = r == 0 && b > 102 && g < 77; + let alti = alt[pos]; + // Compute contours (chunks are assigned in the river code below) + let altj = rescale_height(scale_height_big(alti)); + let contour_interval = 150.0; + let chunk_contour = (altj * gain / contour_interval) as u32; + + // Compute downhill. + let downhill = { + let mut best = -1; + let mut besth = alti; + for nposi in neighbors(*map_size_lg, posi) { + let nbh = alt.raw()[nposi]; + let nalt = rescale_height(scale_height_big(nbh)); + let nchunk_contour = (nalt * gain / contour_interval) as u32; + if !is_contour_line && chunk_contour > nchunk_contour { + is_contour_line = true; + } + let [nr, ng, nb, _na] = rgba.raw()[nposi].to_le_bytes(); + let n_is_water = nr == 0 && nb > 102 && ng < 77; + + if !is_border && is_water && !n_is_water { + is_border = true; + } + + if nbh < besth { + besth = nbh; + best = nposi as isize; + } + } + best + }; + let downhill_wpos = if downhill < 0 { + None + } else { + Some( + Vec2::new( + (downhill as usize % map_size.x as usize) as i32, + (downhill as usize / map_size.x as usize) as i32, + ) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + ) + }; + (Rgb::new(r, g, b), alti, downhill_wpos) + } else { + (Rgb::zero(), 0, None) + }; + let alt = f64::from(rescale_height(scale_height_big(alt))); + let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); + let downhill_wpos = + downhill_wpos.unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32)); + let is_path = rgb.r == 0x37 && rgb.g == 0x29 && rgb.b == 0x23; + let rgb = rgb.map(|e: u8| e as f64 / 255.0); + let is_water = rgb.r == 0.0 && rgb.b > 0.4 && rgb.g < 0.3; + + let rgb = if is_height_map { + if is_path { + // Path color is Rgb::new(0x37, 0x29, 0x23) + Rgb::new(0.9, 0.9, 0.63) + } else if is_water { + Rgb::new(0.23, 0.47, 0.53) + } else if is_contours && is_contour_line { + // Color contour lines + Rgb::new(0.15, 0.15, 0.15) + } else { + // Color hill shading + let lightness = (alt + 0.2).min(1.0) as f64; + Rgb::new(lightness, 0.9 * lightness, 0.5 * lightness) + } + } else if is_stylized_topo { + if is_path { + Rgb::new(0.9, 0.9, 0.63) + } else if is_water { + if is_border { + Rgb::new(0.10, 0.34, 0.50) + } else { + Rgb::new(0.23, 0.47, 0.63) + } + } else if is_contour_line { + Rgb::new(0.25, 0.25, 0.25) + } else { + // Stylized colors + Rgb::new( + (rgb.r + 0.25).min(1.0), + (rgb.g + 0.23).min(1.0), + (rgb.b + 0.10).min(1.0), + ) + } + } else { + Rgb::new(rgb.r, rgb.g, rgb.b) + } + .map(|e| (e * 255.0) as u8); + common::terrain::map::MapSample { + rgb, + alt, + downhill_wpos, + connections: None, + } + } + // Generate standard shaded map + map_config.is_shaded = true; + map_config.generate( + |pos| { + sample_pos( + &map_config, + pos, + &alt, + &rgba, + &map_size, + &map_size_lg, + max_height, + ) + }, + |wpos| { + let pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); + rescale_height(if bounds_check(pos) { + scale_height_big(alt[pos]) + } else { + 0.0 + }) + }, + |pos, (r, g, b, a)| { + world_map_rgba[pos.y * map_size.x as usize + pos.x] = + u32::from_le_bytes([r, g, b, a]); + }, + ); + // Generate map with topographical lines and stylized colors + map_config.is_contours = true; + map_config.is_stylized_topo = true; + map_config.generate( + |pos| { + sample_pos( + &map_config, + pos, + &alt, + &rgba, + &map_size, + &map_size_lg, + max_height, + ) + }, + |wpos| { + let pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); + rescale_height(if bounds_check(pos) { + scale_height_big(alt[pos]) + } else { + 0.0 + }) + }, + |pos, (r, g, b, a)| { + world_map_topo[pos.y * map_size.x as usize + pos.x] = + u32::from_le_bytes([r, g, b, a]); + }, + ); + let make_raw = |rgb| -> Result<_, Error> { + let mut raw = vec![0u8; 4 * world_map_rgba.len()]; + LittleEndian::write_u32_into(rgb, &mut raw); + Ok(Arc::new( + DynamicImage::ImageRgba8({ + // Should not fail if the dimensions are correct. + let map = + image::ImageBuffer::from_raw(u32::from(map_size.x), u32::from(map_size.y), raw); + map.ok_or_else(|| Error::Other("Server sent a bad world map image".into()))? + }) + // Flip the image, since Voxygen uses an orientation where rotation from + // positive x axis to positive y axis is counterclockwise around the z axis. + .flipv(), + )) + }; + let lod_base = rgba; + let lod_alt = alt; + let world_map_rgb_img = make_raw(&world_map_rgba)?; + let world_map_topo_img = make_raw(&world_map_topo)?; + let world_map_layers = vec![world_map_rgb_img, world_map_topo_img]; + let horizons = (west.0, west.1, east.0, east.1) + .into_par_iter() + .map(|(wa, wh, ea, eh)| u32::from_le_bytes([wa, wh, ea, eh])) + .collect::>(); + let lod_horizon = horizons; + let map_bounds = Vec2::new(sea_level, max_height); + debug!("Done preparing image..."); + + Ok(( + state, + lod_base, + lod_alt, + Grid::from_raw(map_size.map(|e| e as i32), lod_horizon), + (world_map_layers, map_size, map_bounds), + world_map.sites, + world_map.pois, + recipe_book, + component_recipe_book, + max_group_size, + client_timeout, + )) + }); + let ( state, lod_base, @@ -352,312 +660,11 @@ impl Client { component_recipe_book, max_group_size, client_timeout, - ) = match loop { + ) = loop { tokio::select! { - res = register_stream.recv() => break res?, + res = &mut task => break res.expect("Client thread should not panic")?, _ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?, } - } { - ServerInit::GameSync { - entity_package, - time_of_day, - max_group_size, - client_timeout, - world_map, - recipe_book, - component_recipe_book, - material_stats, - ability_map, - } => { - // Initialize `State` - let mut state = State::client(); - // Client-only components - state.ecs_mut().register::>(); - - let entity = state.ecs_mut().apply_entity_package(entity_package); - *state.ecs_mut().write_resource() = time_of_day; - *state.ecs_mut().write_resource() = PlayerEntity(Some(entity)); - state.ecs_mut().insert(material_stats); - state.ecs_mut().insert(ability_map); - - let map_size_lg = common::terrain::MapSizeLg::new(world_map.dimensions_lg) - .map_err(|_| { - Error::Other(format!( - "Server sent bad world map dimensions: {:?}", - world_map.dimensions_lg, - )) - })?; - let map_size = map_size_lg.chunks(); - let max_height = world_map.max_height; - let sea_level = world_map.sea_level; - let rgba = world_map.rgba; - let alt = world_map.alt; - if rgba.size() != map_size.map(|e| e as i32) { - return Err(Error::Other("Server sent a bad world map image".into())); - } - if alt.size() != map_size.map(|e| e as i32) { - return Err(Error::Other("Server sent a bad altitude map.".into())); - } - let [west, east] = world_map.horizons; - let scale_angle = - |a: u8| (a as f32 / 255.0 * ::FRAC_PI_2()).tan(); - let scale_height = |h: u8| h as f32 / 255.0 * max_height; - let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height; - ping_stream.send(PingMsg::Ping)?; - - debug!("Preparing image..."); - let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| { - ( - angles.iter().copied().map(scale_angle).collect::>(), - heights - .iter() - .copied() - .map(scale_height) - .collect::>(), - ) - }; - let horizons = [unzip_horizons(&west), unzip_horizons(&east)]; - - // Redraw map (with shadows this time). - let mut world_map_rgba = vec![0u32; rgba.size().product() as usize]; - let mut world_map_topo = vec![0u32; rgba.size().product() as usize]; - let mut map_config = common::terrain::map::MapConfig::orthographic( - map_size_lg, - core::ops::RangeInclusive::new(0.0, max_height), - ); - map_config.horizons = Some(&horizons); - let rescale_height = |h: f32| h / max_height; - let bounds_check = |pos: Vec2| { - pos.reduce_partial_min() >= 0 - && pos.x < map_size.x as i32 - && pos.y < map_size.y as i32 - }; - ping_stream.send(PingMsg::Ping)?; - fn sample_pos( - map_config: &MapConfig, - pos: Vec2, - alt: &Grid, - rgba: &Grid, - map_size: &Vec2, - map_size_lg: &common::terrain::MapSizeLg, - max_height: f32, - ) -> common::terrain::map::MapSample { - let rescale_height = |h: f32| h / max_height; - let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height; - let bounds_check = |pos: Vec2| { - pos.reduce_partial_min() >= 0 - && pos.x < map_size.x as i32 - && pos.y < map_size.y as i32 - }; - let MapConfig { - gain, - is_contours, - is_height_map, - is_stylized_topo, - .. - } = *map_config; - let mut is_contour_line = false; - let mut is_border = false; - let (rgb, alt, downhill_wpos) = if bounds_check(pos) { - let posi = pos.y as usize * map_size.x as usize + pos.x as usize; - let [r, g, b, _a] = rgba[pos].to_le_bytes(); - let is_water = r == 0 && b > 102 && g < 77; - let alti = alt[pos]; - // Compute contours (chunks are assigned in the river code below) - let altj = rescale_height(scale_height_big(alti)); - let contour_interval = 150.0; - let chunk_contour = (altj * gain / contour_interval) as u32; - - // Compute downhill. - let downhill = { - let mut best = -1; - let mut besth = alti; - for nposi in neighbors(*map_size_lg, posi) { - let nbh = alt.raw()[nposi]; - let nalt = rescale_height(scale_height_big(nbh)); - let nchunk_contour = (nalt * gain / contour_interval) as u32; - if !is_contour_line && chunk_contour > nchunk_contour { - is_contour_line = true; - } - let [nr, ng, nb, _na] = rgba.raw()[nposi].to_le_bytes(); - let n_is_water = nr == 0 && nb > 102 && ng < 77; - - if !is_border && is_water && !n_is_water { - is_border = true; - } - - if nbh < besth { - besth = nbh; - best = nposi as isize; - } - } - best - }; - let downhill_wpos = if downhill < 0 { - None - } else { - Some( - Vec2::new( - (downhill as usize % map_size.x as usize) as i32, - (downhill as usize / map_size.x as usize) as i32, - ) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), - ) - }; - (Rgb::new(r, g, b), alti, downhill_wpos) - } else { - (Rgb::zero(), 0, None) - }; - let alt = f64::from(rescale_height(scale_height_big(alt))); - let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); - let downhill_wpos = downhill_wpos - .unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32)); - let is_path = rgb.r == 0x37 && rgb.g == 0x29 && rgb.b == 0x23; - let rgb = rgb.map(|e: u8| e as f64 / 255.0); - let is_water = rgb.r == 0.0 && rgb.b > 0.4 && rgb.g < 0.3; - - let rgb = if is_height_map { - if is_path { - // Path color is Rgb::new(0x37, 0x29, 0x23) - Rgb::new(0.9, 0.9, 0.63) - } else if is_water { - Rgb::new(0.23, 0.47, 0.53) - } else if is_contours && is_contour_line { - // Color contour lines - Rgb::new(0.15, 0.15, 0.15) - } else { - // Color hill shading - let lightness = (alt + 0.2).min(1.0) as f64; - Rgb::new(lightness, 0.9 * lightness, 0.5 * lightness) - } - } else if is_stylized_topo { - if is_path { - Rgb::new(0.9, 0.9, 0.63) - } else if is_water { - if is_border { - Rgb::new(0.10, 0.34, 0.50) - } else { - Rgb::new(0.23, 0.47, 0.63) - } - } else if is_contour_line { - Rgb::new(0.25, 0.25, 0.25) - } else { - // Stylized colors - Rgb::new( - (rgb.r + 0.25).min(1.0), - (rgb.g + 0.23).min(1.0), - (rgb.b + 0.10).min(1.0), - ) - } - } else { - Rgb::new(rgb.r, rgb.g, rgb.b) - } - .map(|e| (e * 255.0) as u8); - common::terrain::map::MapSample { - rgb, - alt, - downhill_wpos, - connections: None, - } - } - // Generate standard shaded map - map_config.is_shaded = true; - map_config.generate( - |pos| { - sample_pos( - &map_config, - pos, - &alt, - &rgba, - &map_size, - &map_size_lg, - max_height, - ) - }, - |wpos| { - let pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); - rescale_height(if bounds_check(pos) { - scale_height_big(alt[pos]) - } else { - 0.0 - }) - }, - |pos, (r, g, b, a)| { - world_map_rgba[pos.y * map_size.x as usize + pos.x] = - u32::from_le_bytes([r, g, b, a]); - }, - ); - // Generate map with topographical lines and stylized colors - map_config.is_contours = true; - map_config.is_stylized_topo = true; - map_config.generate( - |pos| { - sample_pos( - &map_config, - pos, - &alt, - &rgba, - &map_size, - &map_size_lg, - max_height, - ) - }, - |wpos| { - let pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); - rescale_height(if bounds_check(pos) { - scale_height_big(alt[pos]) - } else { - 0.0 - }) - }, - |pos, (r, g, b, a)| { - world_map_topo[pos.y * map_size.x as usize + pos.x] = - u32::from_le_bytes([r, g, b, a]); - }, - ); - ping_stream.send(PingMsg::Ping)?; - let make_raw = |rgb| -> Result<_, Error> { - let mut raw = vec![0u8; 4 * world_map_rgba.len()]; - LittleEndian::write_u32_into(rgb, &mut raw); - Ok(Arc::new( - DynamicImage::ImageRgba8({ - // Should not fail if the dimensions are correct. - let map = - image::ImageBuffer::from_raw(u32::from(map_size.x), u32::from(map_size.y), raw); - map.ok_or_else(|| Error::Other("Server sent a bad world map image".into()))? - }) - // Flip the image, since Voxygen uses an orientation where rotation from - // positive x axis to positive y axis is counterclockwise around the z axis. - .flipv(), - )) - }; - ping_stream.send(PingMsg::Ping)?; - let lod_base = rgba; - let lod_alt = alt; - let world_map_rgb_img = make_raw(&world_map_rgba)?; - let world_map_topo_img = make_raw(&world_map_topo)?; - let world_map_layers = vec![world_map_rgb_img, world_map_topo_img]; - let horizons = (west.0, west.1, east.0, east.1) - .into_par_iter() - .map(|(wa, wh, ea, eh)| u32::from_le_bytes([wa, wh, ea, eh])) - .collect::>(); - let lod_horizon = horizons; - let map_bounds = Vec2::new(sea_level, max_height); - debug!("Done preparing image..."); - - ( - state, - lod_base, - lod_alt, - Grid::from_raw(map_size.map(|e| e as i32), lod_horizon), - (world_map_layers, map_size, map_bounds), - world_map.sites, - world_map.pois, - recipe_book, - component_recipe_book, - max_group_size, - client_timeout, - ) - }, }; ping_stream.send(PingMsg::Ping)?; @@ -1899,7 +1906,19 @@ impl Client { ]; for key in keys.iter() { - if self.state.terrain().get_key(*key).is_none() { + let dist_to_player = (TerrainGrid::key_chunk(*key).map(|x| x as f32) + + TerrainChunkSize::RECT_SIZE.map(|x| x as f32) / 2.0) + .distance_squared(pos.0.into()); + + let terrain = self.state.terrain(); + if let Some(chunk) = terrain.get_key_arc(*key) { + if !skip_mode && !terrain.contains_key_real(*key) { + let chunk = Arc::clone(chunk); + drop(terrain); + self.state.insert_chunk(*key, chunk); + } + } else { + drop(terrain); if !skip_mode && !self.pending_chunks.contains_key(key) { const TOTAL_PENDING_CHUNKS_LIMIT: usize = 12; const CURRENT_TICK_PENDING_CHUNKS_LIMIT: usize = 2; @@ -1917,11 +1936,6 @@ impl Client { } } - let dist_to_player = - (self.state.terrain().key_pos(*key).map(|x| x as f32) - + TerrainChunkSize::RECT_SIZE.map(|x| x as f32) / 2.0) - .distance_squared(pos.0.into()); - if dist_to_player < self.loaded_distance { self.loaded_distance = dist_to_player; } @@ -2510,7 +2524,12 @@ impl Client { } // ignore network events - while let Some(Ok(Some(event))) = self.participant.as_ref().map(|p| p.try_fetch_event()) { + while let Some(res) = self + .participant + .as_mut() + .and_then(|p| p.try_fetch_event().transpose()) + { + let event = res?; trace!(?event, "received network event"); } @@ -2875,8 +2894,12 @@ impl Client { self.state.read_storage().get(self.entity()).cloned(), self.state.read_storage().get(self.entity()).cloned(), ) { - self.in_game_stream - .send(ClientGeneral::PlayerPhysics { pos, vel, ori })?; + self.in_game_stream.send(ClientGeneral::PlayerPhysics { + pos, + vel, + ori, + force_counter: self.force_update_counter, + })?; } } diff --git a/common/frontend/Cargo.toml b/common/frontend/Cargo.toml index 2cf82e68d9..e123a8ca7a 100644 --- a/common/frontend/Cargo.toml +++ b/common/frontend/Cargo.toml @@ -19,4 +19,4 @@ tracing-log = "0.1.1" tracing-subscriber = { version = "0.3.7", default-features = false, features = ["env-filter", "fmt", "time", "ansi", "smallvec", "tracing-log"]} # Tracy -tracing-tracy = { version = "0.10.0", optional = true } +tracing-tracy = { version = "0.9.0", optional = true } diff --git a/common/net/src/msg/world_msg.rs b/common/net/src/msg/world_msg.rs index 1ab33ee89c..16986f1d0b 100644 --- a/common/net/src/msg/world_msg.rs +++ b/common/net/src/msg/world_msg.rs @@ -1,6 +1,6 @@ -use common::{grid::Grid, trade::Good}; +use common::{grid::Grid, terrain::TerrainChunk, trade::Good}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use vek::*; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -26,8 +26,6 @@ pub struct WorldMapMsg { /// /// NOTE: Invariant: chunk count fits in a u16. pub dimensions_lg: Vec2, - /// Sea level (used to provide a base altitude). - pub sea_level: f32, /// Max height (used to scale altitudes). pub max_height: f32, /// RGB+A; the alpha channel is currently unused, but will be used in the @@ -124,6 +122,10 @@ pub struct WorldMapMsg { pub horizons: [(Vec, Vec); 2], pub sites: Vec, pub pois: Vec, + /// Default chunk (representing the ocean outside the map bounds). Sea + /// level (used to provide a base altitude) is the lower bound of this + /// chunk. + pub default_chunk: Arc, } pub type SiteId = common::trade::SiteId; diff --git a/common/src/terrain/map.rs b/common/src/terrain/map.rs index 5fb3101e46..e2e1b2f371 100644 --- a/common/src/terrain/map.rs +++ b/common/src/terrain/map.rs @@ -132,7 +132,7 @@ pub const MAX_WORLD_BLOCKS_LG: Vec2 = Vec2 { x: 19, y: 19 }; /// [TERRAIN_CHUNK_BLOCKS_LG]))` fits in an i32 (derived from the invariant /// on [MAX_WORLD_BLOCKS_LG]). /// -/// NOTE: As an invariant, each dimension (in chunks) must fit in a u16. +/// NOTE: As an invariant, each dimension (in chunks) must fit in a i16. /// /// NOTE: As an invariant, the product of dimensions (in chunks) must fit in a /// usize. @@ -160,12 +160,12 @@ impl MapSizeLg { // 0 and ([MAX_WORLD_BLOCKS_LG] - [TERRAIN_CHUNK_BLOCKS_LG]) let is_le_max = map_size_lg.x <= MAX_WORLD_BLOCKS_LG.x - TERRAIN_CHUNK_BLOCKS_LG && map_size_lg.y <= MAX_WORLD_BLOCKS_LG.y - TERRAIN_CHUNK_BLOCKS_LG; - // Assertion on dimensions: chunks must fit in a u16. + // Assertion on dimensions: chunks must fit in a i16. let chunks_in_range = - /* 1u16.checked_shl(map_size_lg.x).is_some() && - 1u16.checked_shl(map_size_lg.y).is_some(); */ - map_size_lg.x <= 16 && - map_size_lg.y <= 16; + /* 1u15.checked_shl(map_size_lg.x).is_some() && + 1u15.checked_shl(map_size_lg.y).is_some(); */ + map_size_lg.x <= 15 && + map_size_lg.y <= 15; if is_le_max && chunks_in_range { // Assertion on dimensions: blocks must fit in a i32. let blocks_in_range = @@ -197,6 +197,15 @@ impl MapSizeLg { /// Get the size of an array of the correct size to hold all chunks. pub const fn chunks_len(self) -> usize { 1 << (self.0.x + self.0.y) } + + #[inline(always)] + /// Determine whether a chunk position is in bounds. + pub const fn contains_chunk(&self, chunk_key: Vec2) -> bool { + let map_size = self.chunks(); + chunk_key.x >= 0 && chunk_key.y >= 0 && + chunk_key.x == chunk_key.x & ((map_size.x as i32) - 1) && + chunk_key.y == chunk_key.y & ((map_size.y as i32) - 1) + } } impl From for Vec2 { diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index c49cdf7968..a9f065fb48 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -188,6 +188,16 @@ impl TerrainGrid { } impl TerrainChunk { + /// Generate an all-water chunk at a specific sea level. + pub fn water(sea_level: i32) -> TerrainChunk { + TerrainChunk::new( + sea_level, + Block::new(BlockKind::Water, Rgb::zero()), + Block::air(SpriteKind::Empty), + TerrainChunkMeta::void(), + ) + } + /// Find the highest or lowest accessible position within the chunk pub fn find_accessible_pos(&self, spawn_wpos: Vec2, ascending: bool) -> Vec3 { let min_z = self.get_min_z(); diff --git a/common/src/volumes/vol_grid_2d.rs b/common/src/volumes/vol_grid_2d.rs index 49c91f00fc..9a40bd9eec 100644 --- a/common/src/volumes/vol_grid_2d.rs +++ b/common/src/volumes/vol_grid_2d.rs @@ -1,4 +1,5 @@ use crate::{ + terrain::MapSizeLg, vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol, WriteVol}, volumes::dyna::DynaError, }; @@ -19,6 +20,10 @@ pub enum VolGrid2dError { // M = Chunk metadata #[derive(Clone)] pub struct VolGrid2d { + /// Size of the entire (not just loaded) map. + map_size_lg: MapSizeLg, + /// Default voxel for use outside of max map bounds. + default: Arc, chunks: HashMap, Arc>, } @@ -29,6 +34,18 @@ impl VolGrid2d { .map2(V::RECT_SIZE, |e, sz: u32| e.div_euclid(sz as i32)) } + #[inline(always)] + pub fn key_chunk>>(key: K) -> Vec2 { + key.into() * V::RECT_SIZE.map(|e| e as i32) + } + + #[inline(always)] + pub fn par_keys(&self) -> hashbrown::hash_map::rayon::ParKeys, Arc> + where V: Send + Sync, + { + self.chunks.par_keys() + } + #[inline(always)] pub fn chunk_offs(pos: Vec3) -> Vec3 { let offs = Vec2::::from(pos).map2(V::RECT_SIZE, |e, sz| e & (sz - 1) as i32); @@ -45,8 +62,7 @@ impl ReadVol for VolGrid2d { #[inline(always)] fn get(&self, pos: Vec3) -> Result<&V::Vox, VolGrid2dError> { let ck = Self::chunk_key(pos); - self.chunks - .get(&ck) + self.get_key(ck) .ok_or(VolGrid2dError::NoSuchChunk) .and_then(|chunk| { let co = Self::chunk_offs(pos); @@ -102,14 +118,14 @@ impl>, V: RectRasterableVol + ReadVol + Debug> SampleVol fo fn sample(&self, range: I) -> Result> { let range = range.into(); - let mut sample = VolGrid2d::new()?; + let mut sample = VolGrid2d::new(self.map_size_lg, Arc::clone(&self.default))?; let chunk_min = Self::chunk_key(range.min); let chunk_max = Self::chunk_key(range.max); for x in chunk_min.x..chunk_max.x + 1 { for y in chunk_min.y..chunk_max.y + 1 { let chunk_key = Vec2::new(x, y); - let chunk = self.get_key_arc(chunk_key).cloned(); + let chunk = self.get_key_arc_real(chunk_key).cloned(); if let Some(chunk) = chunk { sample.insert(chunk_key, chunk); @@ -138,12 +154,14 @@ impl WriteVol for VolGrid2d } impl VolGrid2d { - pub fn new() -> Result> { + pub fn new(map_size_lg: MapSizeLg, default: Arc) -> Result> { if Self::chunk_size() .map(|e| e.is_power_of_two() && e > 0) .reduce_and() { Ok(Self { + map_size_lg, + default, chunks: HashMap::default(), }) } else { @@ -160,10 +178,37 @@ impl VolGrid2d { #[inline(always)] pub fn get_key(&self, key: Vec2) -> Option<&V> { - self.chunks.get(&key).map(|arc_chunk| arc_chunk.as_ref()) + self.get_key_arc(key).map(|arc_chunk| arc_chunk.as_ref()) } - pub fn get_key_arc(&self, key: Vec2) -> Option<&Arc> { self.chunks.get(&key) } + #[inline(always)] + pub fn get_key_real(&self, key: Vec2) -> Option<&V> { + self.get_key_arc_real(key).map(|arc_chunk| arc_chunk.as_ref()) + } + + #[inline(always)] + pub fn contains_key(&self, key: Vec2) -> bool { + self.contains_key_real(key) || + // Counterintuitively, areas outside the map are *always* considered to be in it, since + // they're assigned the default chunk. + !self.map_size_lg.contains_chunk(key) + } + + #[inline(always)] + pub fn contains_key_real(&self, key: Vec2) -> bool { + self.chunks.contains_key(&key) + } + + #[inline(always)] + pub fn get_key_arc(&self, key: Vec2) -> Option<&Arc> { + self.get_key_arc_real(key) + .or_else(|| if !self.map_size_lg.contains_chunk(key) { Some(&self.default) } else { None }) + } + + #[inline(always)] + pub fn get_key_arc_real(&self, key: Vec2) -> Option<&Arc> { + self.chunks.get(&key) + } pub fn clear(&mut self) { self.chunks.clear(); } @@ -172,7 +217,7 @@ impl VolGrid2d { pub fn remove(&mut self, key: Vec2) -> Option> { self.chunks.remove(&key) } #[inline(always)] - pub fn key_pos(&self, key: Vec2) -> Vec2 { key * V::RECT_SIZE.map(|e| e as i32) } + pub fn key_pos(&self, key: Vec2) -> Vec2 { Self::key_chunk(key) } #[inline(always)] pub fn pos_key(&self, pos: Vec3) -> Vec2 { Self::chunk_key(pos) } @@ -219,8 +264,7 @@ impl<'a, V: RectRasterableVol + ReadVol> CachedVolGrid2d<'a, V> { // Otherwise retrieve from the hashmap let chunk = self .vol_grid_2d - .chunks - .get(&ck) + .get_key_arc(ck) .ok_or(VolGrid2dError::NoSuchChunk)?; // Store most recently looked up chunk in the cache self.cache = Some((ck, Arc::clone(chunk))); diff --git a/common/state/src/state.rs b/common/state/src/state.rs index d088bc84d3..de1d7c7d3b 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -17,7 +17,7 @@ use common::{ TimeOfDay, }, slowjob::SlowJobPool, - terrain::{Block, TerrainChunk, TerrainGrid}, + terrain::{Block, MapSizeLg, TerrainChunk, TerrainGrid}, time::DayPeriod, trade::Trades, vol::{ReadVol, WriteVol}, @@ -94,37 +94,56 @@ pub struct State { thread_pool: Arc, } +pub type Pools = Arc; + impl State { - /// Create a new `State` in client mode. - pub fn client() -> Self { Self::new(GameMode::Client) } - - /// Create a new `State` in server mode. - pub fn server() -> Self { Self::new(GameMode::Server) } - - pub fn new(game_mode: GameMode) -> Self { + pub fn pools(game_mode: GameMode) -> Pools { let thread_name_infix = match game_mode { GameMode::Server => "s", GameMode::Client => "c", GameMode::Singleplayer => "sp", }; - let thread_pool = Arc::new( + Arc::new( ThreadPoolBuilder::new() .num_threads(num_cpus::get().max(common::consts::MIN_RECOMMENDED_RAYON_THREADS)) .thread_name(move |i| format!("rayon-{}-{}", thread_name_infix, i)) .build() .unwrap(), - ); + ) + } + + /// Create a new `State` in client mode. + pub fn client(pools: Pools, map_size_lg: MapSizeLg, default_chunk: Arc) -> Self { + Self::new(GameMode::Client, pools, map_size_lg, default_chunk) + } + + /// Create a new `State` in server mode. + pub fn server(pools: Pools, map_size_lg: MapSizeLg, default_chunk: Arc) -> Self { + Self::new(GameMode::Server, pools, map_size_lg, default_chunk) + } + + pub fn new( + game_mode: GameMode, + pools: Pools, + map_size_lg: MapSizeLg, + default_chunk: Arc, + ) -> Self { Self { - ecs: Self::setup_ecs_world(game_mode, &thread_pool), - thread_pool, + ecs: Self::setup_ecs_world(game_mode, Arc::clone(&pools), map_size_lg, default_chunk), + thread_pool: pools, } } /// Creates ecs world and registers all the common components and resources // TODO: Split up registering into server and client (e.g. move // EventBus to the server) - fn setup_ecs_world(game_mode: GameMode, thread_pool: &Arc) -> specs::World { + fn setup_ecs_world( + game_mode: GameMode, + thread_pool: Arc, + map_size_lg: MapSizeLg, + default_chunk: Arc, + ) -> specs::World { let mut ecs = specs::World::new(); // Uids for sync ecs.register_sync_marker(); @@ -213,7 +232,7 @@ impl State { ecs.insert(Time(0.0)); ecs.insert(DeltaTime(0.0)); ecs.insert(PlayerEntity(None)); - ecs.insert(TerrainGrid::new().unwrap()); + ecs.insert(TerrainGrid::new(map_size_lg, default_chunk).unwrap()); ecs.insert(BlockChange::default()); ecs.insert(crate::build_areas::BuildAreas::default()); ecs.insert(TerrainChanges::default()); @@ -226,11 +245,7 @@ impl State { let num_cpu = num_cpus::get() as u64; let slow_limit = (num_cpu / 2 + num_cpu / 4).max(1); tracing::trace!(?slow_limit, "Slow Thread limit"); - ecs.insert(SlowJobPool::new( - slow_limit, - 10_000, - Arc::clone(thread_pool), - )); + ecs.insert(SlowJobPool::new(slow_limit, 10_000, thread_pool)); // TODO: only register on the server ecs.insert(EventBus::::default()); diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 7c2b98c138..f3d50dcd58 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -632,8 +632,7 @@ impl<'a> PhysicsData<'a> { )| { let in_loaded_chunk = read .terrain - .get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) - .is_some(); + .contains_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))); // Apply physics only if in a loaded chunk if in_loaded_chunk @@ -790,8 +789,7 @@ impl<'a> PhysicsData<'a> { let in_loaded_chunk = read .terrain - .get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) - .is_some(); + .contains_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))); // Don't move if we're not in a loaded chunk let pos_delta = if in_loaded_chunk { diff --git a/common/systems/tests/character_state.rs b/common/systems/tests/character_state.rs index 77db306723..467308c546 100644 --- a/common/systems/tests/character_state.rs +++ b/common/systems/tests/character_state.rs @@ -6,6 +6,7 @@ mod tests { Ori, PhysicsState, Poise, Pos, Skill, Stats, Vel, }, resources::{DeltaTime, GameMode, Time}, + terrain::{MapSizeLg, TerrainChunk}, uid::Uid, util::Dir, SkillSetBuilder, @@ -14,12 +15,25 @@ mod tests { use common_state::State; use rand::thread_rng; use specs::{Builder, Entity, WorldExt}; - use std::time::Duration; - use vek::{approx::AbsDiffEq, Vec3}; + use std::{sync::Arc, time::Duration}; + use vek::{approx::AbsDiffEq, Vec2, Vec3}; use veloren_common_systems::character_behavior; + const DEFAULT_WORLD_CHUNKS_LG: MapSizeLg = + if let Ok(map_size_lg) = MapSizeLg::new(Vec2 { x: 1, y: 1 }) { + map_size_lg + } else { + panic!("Default world chunk size does not satisfy required invariants."); + }; + fn setup() -> State { - let mut state = State::new(GameMode::Server); + let pools = State::pools(GameMode::Server); + let mut state = State::new( + GameMode::Server, + pools, + DEFAULT_WORLD_CHUNKS_LG, + Arc::new(TerrainChunk::water(0)), + ); let msm = MaterialStatManifest::load().cloned(); state.ecs_mut().insert(msm); state.ecs_mut().read_resource::