From 3b424e904943825382c43e932b5fe7e37a7d6b7d Mon Sep 17 00:00:00 2001 From: Joshua Yanovski Date: Sun, 11 Sep 2022 10:16:31 -0700 Subject: [PATCH] Significantly optimizing terrain::Sys::run. --- client/src/bin/swarm/main.rs | 57 +- client/src/lib.rs | 633 +++++++++--------- common/net/src/msg/world_msg.rs | 8 +- common/src/terrain/map.rs | 21 +- common/src/volumes/vol_grid_2d.rs | 45 +- common/state/src/state.rs | 23 +- common/systems/src/phys.rs | 6 +- common/systems/tests/phys/utils.rs | 10 +- server/Cargo.toml | 1 + server/src/chunk_generator.rs | 8 + server/src/connection_handler.rs | 19 +- server/src/lib.rs | 77 ++- server/src/sys/chunk_serialize.rs | 2 +- server/src/sys/msg/terrain.rs | 6 +- server/src/sys/terrain.rs | 384 ++++++++--- server/src/sys/terrain_sync.rs | 68 +- server/src/test_world.rs | 9 + voxygen/benches/meshing_benchmark.rs | 2 +- voxygen/src/scene/terrain.rs | 4 +- .../examples/chunk_compression_benchmarks.rs | 2 +- world/src/lib.rs | 12 +- world/src/sim/mod.rs | 16 +- 22 files changed, 886 insertions(+), 527 deletions(-) diff --git a/client/src/bin/swarm/main.rs b/client/src/bin/swarm/main.rs index 646b9c4834..b7d78b6610 100644 --- a/client/src/bin/swarm/main.rs +++ b/client/src/bin/swarm/main.rs @@ -31,7 +31,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 @@ -49,6 +49,8 @@ fn main() { let finished_init = Arc::new(AtomicU32::new(0)); let runtime = Arc::new(Runtime::new().unwrap()); + let mut pools = common_state::State::pools(common::resources::GameMode::Client); + pools.0 = 0; // TODO: calculate and log the required chunks per second to maintain the // selected scenario with full vd loaded @@ -58,6 +60,7 @@ fn main() { 0, to_adminify, &runtime, + &pools, opt, &finished_init, ); @@ -68,14 +71,13 @@ fn main() { index as u32, Vec::new(), &runtime, + &pools, opt, &finished_init, ); }); - loop { - thread::sleep(Duration::from_secs_f32(1.0)); - } + std::thread::park(); } fn run_client_new_thread( @@ -83,13 +85,15 @@ fn run_client_new_thread( index: u32, to_adminify: Vec, runtime: &Arc, + pools: &common_state::Pools, opt: Opt, finished_init: &Arc, ) { let runtime = Arc::clone(runtime); + let pools = pools.clone(); let finished_init = Arc::clone(finished_init); thread::spawn(move || { - if let Err(err) = run_client(username, index, to_adminify, runtime, opt, finished_init) { + if let Err(err) = run_client(username, index, to_adminify, pools, runtime, opt, finished_init) { tracing::error!("swarm member {} exited with an error: {:?}", index, err); } }); @@ -99,29 +103,34 @@ fn run_client( username: String, index: u32, to_adminify: Vec, + pools: common_state::Pools, runtime: Arc, opt: Opt, finished_init: Arc, ) -> Result<(), veloren_client::Error> { - // Connect to localhost - let addr = ConnectionArgs::Tcp { - prefer_ipv6: false, - hostname: "localhost".into(), + 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, + pools.clone(), + &username, + "", + |_| false, + )) { + Err(e) => tracing::warn!(?e, "Client {} disconnected", index), + Ok(client) => break client, + } }; - let runtime_clone = Arc::clone(&runtime); - // NOTE: use a no-auth server - let mut client = runtime - .block_on(Client::new( - addr, - runtime_clone, - &mut None, - common_state::State::pools(common::resources::GameMode::Client), - &username, - "", - |_| false, - )) - .expect("Failed to connect to the server"); - client.set_view_distance(opt.vd); + drop(pools); let mut clock = common::clock::Clock::new(Duration::from_secs_f32(1.0 / 30.0)); @@ -165,6 +174,8 @@ fn run_client( .expect("Why is this an option?"), ); + client.set_view_distance(opt.vd); + // If this is the admin client then adminify the other swarm members if !to_adminify.is_empty() { // Wait for other clients to connect diff --git a/client/src/lib.rs b/client/src/lib.rs index 02afdda5f5..ff124c8d6d 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -390,315 +390,316 @@ impl Client { component_recipe_book, max_group_size, client_timeout, - ) = match loop { + ) = loop { tokio::select! { - res = register_stream.recv() => break res?, + // Spawn in a blocking thread (leaving the network thread free). This is mostly + // useful for bots. + res = register_stream.recv() => { + let ServerInit::GameSync { + entity_package, + time_of_day, + max_group_size, + client_timeout, + world_map, + recipe_book, + component_recipe_book, + material_stats, + ability_map, + } = res?; + + break 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 mut state = State::client(pools, map_size_lg, world_map.default_chunk); + // Client-only components + state.ecs_mut().register::>(); + state.ecs_mut().write_resource::() + .configure(&"TERRAIN_DROP", |_n| 1); + /* state.ecs_mut().write_resource::() + .configure("TERRAIN_DESERIALIZING", |n| n / 2); */ + 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, + )) + }).await.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(pools); - // Client-only components - state.ecs_mut().register::>(); - state.ecs_mut().write_resource::() - .configure(&"TERRAIN_DROP", |_n| 1); - /* state.ecs_mut().write_resource::() - .configure("TERRAIN_DESERIALIZING", |n| n / 2); */ - 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)?; @@ -1894,7 +1895,20 @@ impl Client { ]; for key in keys.iter() { - if self.state.terrain().get_key(*key).is_none() { + 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()); + + let mut 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 CURRENT_TICK_PENDING_CHUNKS_LIMIT: usize = 8 * 4; if self.pending_chunks.len() < TOTAL_PENDING_CHUNKS_LIMIT @@ -1911,11 +1925,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; } diff --git a/common/net/src/msg/world_msg.rs b/common/net/src/msg/world_msg.rs index b9369b368f..92d286013e 100644 --- a/common/net/src/msg/world_msg.rs +++ b/common/net/src/msg/world_msg.rs @@ -1,7 +1,8 @@ -use common::{grid::Grid, trade::Good}; +use common::{grid::Grid, terrain::TerrainChunk, trade::Good}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, Bytes}; use std::collections::HashMap; +use std::sync::Arc; use vek::*; #[serde_as] @@ -28,8 +29,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 @@ -127,6 +126,9 @@ 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/volumes/vol_grid_2d.rs b/common/src/volumes/vol_grid_2d.rs index 59537eda60..b18ef18174 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>, } @@ -52,8 +57,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); @@ -109,14 +113,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); @@ -145,12 +149,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 { @@ -167,15 +173,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()) + } + + #[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) } - pub fn get_key_arc(&self, key: Vec2) -> Option<&Arc> { self.chunks.get(&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(); } @@ -231,8 +259,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 2bee8bb7a1..17e29152e6 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -17,7 +17,7 @@ use common::{ TimeOfDay, }, slowjob::{self, SlowJobPool}, - terrain::{Block, TerrainChunk, TerrainGrid}, + terrain::{Block, MapSizeLg, TerrainChunk, TerrainGrid}, time::DayPeriod, trade::Trades, vol::{ReadVol, WriteVol}, @@ -179,12 +179,16 @@ impl State { } /// Create a new `State` in client mode. - pub fn client(pools: Pools) -> Self { Self::new(GameMode::Client, pools) } + 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) -> Self { Self::new(GameMode::Server, pools) } + 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(ecs_role: GameMode, pools: Pools) -> Self { + pub fn new(ecs_role: GameMode, pools: Pools, map_size_lg: MapSizeLg, default_chunk: Arc) -> Self { /* let thread_name_infix = match game_mode { GameMode::Server => "s", GameMode::Client => "c", @@ -243,7 +247,7 @@ impl State { ); Self { - ecs: Self::setup_ecs_world(ecs_role, /*num_cpu as u64*//*, &thread_pool, *//*pools.1*/slowjob/*pools.3*/), + ecs: Self::setup_ecs_world(ecs_role, /*num_cpu as u64*//*, &thread_pool, *//*pools.1*/slowjob/*pools.3*/, map_size_lg, default_chunk), thread_pool: pools.2, } } @@ -251,7 +255,12 @@ impl State { /// 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(ecs_role: GameMode, /*num_cpu: u64*//*, thread_pool: &Arc, */slowjob: SlowJobPool) -> specs::World { + fn setup_ecs_world( + ecs_role: GameMode, /*num_cpu: u64*//*, thread_pool: &Arc, */ + slowjob: SlowJobPool, + map_size_lg: MapSizeLg, + default_chunk: Arc, + ) -> specs::World { let mut ecs = specs::World::new(); // Uids for sync ecs.register_sync_marker(); @@ -340,7 +349,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()); diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 86fb126403..e963dc9297 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/phys/utils.rs b/common/systems/tests/phys/utils.rs index 872f1af255..72d2a5eff3 100644 --- a/common/systems/tests/phys/utils.rs +++ b/common/systems/tests/phys/utils.rs @@ -29,7 +29,15 @@ pub fn setup() -> State { state.ecs_mut().insert(MaterialStatManifest::with_empty()); state.ecs_mut().read_resource::