diff --git a/CHANGELOG.md b/CHANGELOG.md index e80e286d45..542c1b35d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - /skill_preset command which allows you to apply skill presets - Added timed bans and ban history. - Added non-admin moderators with limit privileges and updated the security model to reflect this. +- Added a minimap mode that visualizes terrain within a chunk. - Chat tabs - NPC's now hear certain sounds - Renamed Animal Trainers to Beastmasters and gave them their own set of armor to wear diff --git a/Cargo.lock b/Cargo.lock index 3fb5eb69dd..8b11e3bcf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1032,20 +1032,6 @@ dependencies = [ "itertools 0.9.0", ] -[[package]] -name = "crossbeam" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd01a6eb3daaafa260f6fc94c3a6c36390abc2080e38e3e34ced87393fb77d80" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-channel", - "crossbeam-deque 0.8.0", - "crossbeam-epoch 0.9.3", - "crossbeam-queue", - "crossbeam-utils 0.8.3", -] - [[package]] name = "crossbeam-channel" version = "0.5.0" @@ -5815,7 +5801,8 @@ dependencies = [ "coreaudio-sys", "cpal", "criterion", - "crossbeam", + "crossbeam-channel", + "crossbeam-utils 0.8.3", "directories-next", "dispatch 0.1.4", "dot_vox", diff --git a/assets/voxygen/i18n/en/hud/map.ron b/assets/voxygen/i18n/en/hud/map.ron index 2b75841070..5c30f4f22c 100644 --- a/assets/voxygen/i18n/en/hud/map.ron +++ b/assets/voxygen/i18n/en/hud/map.ron @@ -13,7 +13,8 @@ "hud.map.dungeons": "Dungeons", "hud.map.caves": "Caves", "hud.map.cave": "Cave", - "hud.map.peaks": "Mountains", + "hud.map.peaks": "Mountains", + "hud.map.voxel_map": "Voxel map", "hud.map.trees": "Giant Trees", "hud.map.tree": "Giant Tree", "hud.map.town": "Town", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index dfd6c62396..6bd924bd55 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -81,7 +81,8 @@ bincode = "1.3.1" chrono = { version = "0.4.9", features = ["serde"] } cpal = "0.13" copy_dir = "0.1.2" -crossbeam = "0.8.0" +crossbeam-utils = "0.8.1" +crossbeam-channel = "0.5" # TODO: remove directories-next = "2.0" dot_vox = "4.0" diff --git a/voxygen/src/ecs/mod.rs b/voxygen/src/ecs/mod.rs index 701dbb334b..8b7b647b38 100644 --- a/voxygen/src/ecs/mod.rs +++ b/voxygen/src/ecs/mod.rs @@ -11,6 +11,7 @@ pub fn init(world: &mut World) { { let pool = world.read_resource::(); + pool.configure("IMAGE_PROCESSING", |n| n / 2); pool.configure("FIGURE_MESHING", |n| n / 2); pool.configure("TERRAIN_MESHING", |n| n / 2); } diff --git a/voxygen/src/hud/map.rs b/voxygen/src/hud/map.rs index fea6aa9a98..f2389230ad 100644 --- a/voxygen/src/hud/map.rs +++ b/voxygen/src/hud/map.rs @@ -65,6 +65,9 @@ widget_ids! { show_peaks_img, show_peaks_box, show_peaks_text, + show_voxel_map_img, + show_voxel_map_box, + show_voxel_map_text, show_difficulty_img, show_difficulty_box, show_difficulty_text, @@ -200,6 +203,7 @@ impl<'a> Widget for Map<'a> { let show_caves = self.global_state.settings.interface.map_show_caves; let show_trees = self.global_state.settings.interface.map_show_trees; let show_peaks = self.global_state.settings.interface.map_show_peaks; + let show_voxel_map = self.global_state.settings.interface.map_show_voxel_map; let show_topo_map = self.global_state.settings.interface.map_show_topo_map; let mut events = Vec::new(); let i18n = &self.localized_strings; @@ -636,6 +640,44 @@ impl<'a> Widget for Map<'a> { .graphics_for(state.ids.show_peaks_box) .color(TEXT_COLOR) .set(state.ids.show_peaks_text, ui); + // Voxel map (TODO: enable this once Pfau approves the final UI, and once + // there's a non-placeholder graphic for the checkbox) + const EXPOSE_VOXEL_MAP_TOGGLE_IN_UI: bool = false; + if EXPOSE_VOXEL_MAP_TOGGLE_IN_UI { + Image::new(self.imgs.mmap_poi_peak) + .down_from(state.ids.show_peaks_img, 10.0) + .w_h(20.0, 20.0) + .set(state.ids.show_voxel_map_img, ui); + if Button::image(if show_voxel_map { + self.imgs.checkbox_checked + } else { + self.imgs.checkbox + }) + .w_h(18.0, 18.0) + .hover_image(if show_voxel_map { + self.imgs.checkbox_checked_mo + } else { + self.imgs.checkbox_mo + }) + .press_image(if show_voxel_map { + self.imgs.checkbox_checked + } else { + self.imgs.checkbox_press + }) + .right_from(state.ids.show_voxel_map_img, 10.0) + .set(state.ids.show_voxel_map_box, ui) + .was_clicked() + { + events.push(Event::SettingsChange(MapShowVoxelMap(!show_voxel_map))); + } + Text::new(i18n.get("hud.map.voxel_map")) + .right_from(state.ids.show_voxel_map_box, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .graphics_for(state.ids.show_voxel_map_box) + .color(TEXT_COLOR) + .set(state.ids.show_voxel_map_text, ui); + } // Map icons if state.ids.mmap_poi_icons.len() < self.client.pois().len() { state.update(|state| { diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 0fd79bdca8..7eb760a8aa 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -4,22 +4,347 @@ use super::{ TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, }; use crate::{ + hud::{Graphic, Ui}, session::settings_change::{Interface as InterfaceChange, Interface::*}, - ui::{fonts::Fonts, img_ids}, + ui::{fonts::Fonts, img_ids, KeyedJobs}, GlobalState, }; use client::{self, Client}; -use common::{comp, comp::group::Role, terrain::TerrainChunkSize, vol::RectVolSize}; +use common::{ + comp, + comp::group::Role, + grid::Grid, + slowjob::SlowJobPool, + terrain::{Block, BlockKind, TerrainChunk, TerrainChunkSize, TerrainGrid}, + vol::{ReadVol, RectVolSize}, +}; use common_net::msg::world_msg::SiteKind; use conrod_core::{ color, position, widget::{self, Button, Image, Rectangle, Text}, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, }; - +use hashbrown::HashMap; +use image::{DynamicImage, RgbaImage}; use specs::{saveload::MarkerAllocator, WorldExt}; +use std::sync::Arc; use vek::*; +struct MinimapColumn { + /// Coordinate of lowest z-slice + zlo: i32, + /// Z-slices of colors and filled-ness + layers: Vec, bool)>>, + /// Color and filledness above the highest layer + above: (Rgba, bool), + /// Color and filledness below the lowest layer + below: (Rgba, bool), +} + +pub struct VoxelMinimap { + chunk_minimaps: HashMap, MinimapColumn>, + composited: RgbaImage, + image_id: img_ids::Rotations, + last_pos: Vec3, + last_ceiling: i32, + keyed_jobs: KeyedJobs, MinimapColumn>, +} + +const VOXEL_MINIMAP_SIDELENGTH: u32 = 256; + +impl VoxelMinimap { + pub fn new(ui: &mut Ui) -> Self { + let composited = RgbaImage::from_pixel( + VOXEL_MINIMAP_SIDELENGTH, + VOXEL_MINIMAP_SIDELENGTH, + image::Rgba([0, 0, 0, 64]), + ); + Self { + chunk_minimaps: HashMap::new(), + image_id: ui.add_graphic_with_rotations(Graphic::Image( + Arc::new(DynamicImage::ImageRgba8(composited.clone())), + Some(Rgba::from([0.0, 0.0, 0.0, 0.0])), + )), + composited, + last_pos: Vec3::zero(), + last_ceiling: 0, + keyed_jobs: KeyedJobs::new("IMAGE_PROCESSING"), + } + } + + fn block_color(block: &Block) -> Option> { + block + .get_color() + .map(|rgb| Rgba::new(rgb.r, rgb.g, rgb.b, 255)) + .or_else(|| { + matches!(block.kind(), BlockKind::Water).then(|| Rgba::new(119, 149, 197, 255)) + }) + } + + /// Each layer is a slice of the terrain near that z-level + fn composite_layer_slice(chunk: &TerrainChunk, layers: &mut Vec, bool)>>) { + for z in chunk.get_min_z()..chunk.get_max_z() { + let grid = Grid::populate_from(Vec2::new(32, 32), |v| { + let mut rgba = Rgba::::zero(); + let (weights, zoff) = (&[1, 2, 4, 1, 1, 1][..], -2); + for dz in 0..weights.len() { + let color = chunk + .get(Vec3::new(v.x, v.y, dz as i32 + z + zoff)) + .ok() + .and_then(Self::block_color) + .unwrap_or_else(Rgba::zero); + rgba += color.as_() * weights[dz as usize] as f32; + } + let rgba: Rgba = (rgba / weights.iter().map(|x| *x as f32).sum::()).as_(); + (rgba, true) + }); + layers.push(grid); + } + } + + /// Each layer is the overhead as if its z-level were the ceiling + fn composite_layer_overhead(chunk: &TerrainChunk, layers: &mut Vec, bool)>>) { + for z in chunk.get_min_z()..chunk.get_max_z() { + let grid = Grid::populate_from(Vec2::new(32, 32), |v| { + let mut rgba = None; + + let mut seen_solids: u32 = 0; + let mut seen_air: u32 = 0; + for dz in chunk.get_min_z()..=z { + if let Some(color) = chunk + .get(Vec3::new(v.x, v.y, z - dz + chunk.get_min_z())) + .ok() + .and_then(Self::block_color) + { + if seen_air > 0 { + rgba = Some(color); + break; + } + seen_solids += 1; + } else { + seen_air += 1; + } + // Don't penetrate too far into ground, only penetrate through shallow + // ceilings + if seen_solids > 12 { + break; + } + } + let block = chunk.get(Vec3::new(v.x, v.y, z)).ok(); + // Treat Leaves and Wood as translucent for the purposes of ceiling checks, + // since otherwise trees would cause ceiling removal to trigger + // when running under a branch. + let is_filled = block.map_or(true, |b| { + b.is_filled() && !matches!(b.kind(), BlockKind::Leaves | BlockKind::Wood) + }); + let rgba = rgba.unwrap_or_else(|| Rgba::new(0, 0, 0, 255)); + (rgba, is_filled) + }); + layers.push(grid); + } + } + + fn add_chunks_near( + &mut self, + pool: &SlowJobPool, + terrain: &TerrainGrid, + cpos: Vec2, + ) -> bool { + let mut new_chunks = false; + + for (key, chunk) in terrain.iter() { + let delta: Vec2 = (key - cpos).map(i32::abs).as_(); + if delta.x < VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.x + && delta.y < VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.y + && !self.chunk_minimaps.contains_key(&key) + { + if let Some((_, column)) = self.keyed_jobs.spawn(Some(&pool), key, || { + let arc_chunk = Arc::clone(chunk); + move |_| { + let mut layers = Vec::new(); + const MODE_OVERHEAD: bool = true; + if MODE_OVERHEAD { + Self::composite_layer_overhead(&arc_chunk, &mut layers); + } else { + Self::composite_layer_slice(&arc_chunk, &mut layers); + } + let above = arc_chunk + .get(Vec3::new(0, 0, arc_chunk.get_max_z() + 1)) + .ok() + .copied() + .unwrap_or_else(Block::empty); + let below = arc_chunk + .get(Vec3::new(0, 0, arc_chunk.get_min_z() - 1)) + .ok() + .copied() + .unwrap_or_else(Block::empty); + MinimapColumn { + zlo: arc_chunk.get_min_z(), + layers, + above: ( + Self::block_color(&above).unwrap_or_else(Rgba::zero), + above.is_filled(), + ), + below: ( + Self::block_color(&below).unwrap_or_else(Rgba::zero), + below.is_filled(), + ), + } + } + }) { + self.chunk_minimaps.insert(key, column); + new_chunks = true; + } + } + } + new_chunks + } + + fn remove_chunks_far(&mut self, terrain: &TerrainGrid, cpos: Vec2) { + self.chunk_minimaps.retain(|key, _| { + let delta: Vec2 = (key - cpos).map(i32::abs).as_(); + delta.x < 1 + VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.x + && delta.y < 1 + VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.y + && terrain.get_key(*key).is_some() + }); + } + + pub fn maintain(&mut self, client: &Client, ui: &mut Ui) { + let player = client.entity(); + let pos = if let Some(pos) = client.state().ecs().read_storage::().get(player) { + pos.0 + } else { + return; + }; + let vpos = pos.xy() - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0; + let cpos: Vec2 = vpos + .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j)) + .as_(); + + let pool = client.state().ecs().read_resource::(); + let terrain = client.state().terrain(); + let new_chunks = self.add_chunks_near(&pool, &terrain, cpos); + self.remove_chunks_far(&terrain, cpos); + + // ceiling_offset is the distance from the player to a block heuristically + // detected as the ceiling height (a non-tree solid block above them, or + // the sky if no such block exists). This is used for determining which + // z-slice of the minimap to show, such that house roofs and caves and + // dungeons are all handled uniformly. + let ceiling_offset = { + let voff = Vec2::new( + VOXEL_MINIMAP_SIDELENGTH as f32, + VOXEL_MINIMAP_SIDELENGTH as f32, + ) / 2.0; + let coff: Vec2 = voff + .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j)) + .as_(); + let cmod: Vec2 = vpos + .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j)) + .as_(); + let column = self.chunk_minimaps.get(&(cpos + coff)); + column + .map( + |MinimapColumn { + zlo, layers, above, .. + }| { + (0..layers.len() as i32) + .find(|dz| { + layers + .get((pos.z as i32 - zlo + dz) as usize) + .and_then(|grid| grid.get(cmod)) + .map_or(false, |(_, b)| *b) + }) + .unwrap_or_else(|| { + // if the `find` returned None, there's no solid blocks above the + // player within the chunk + if above.1 { + // if the `above` block is solid, the chunk has an infinite + // solid ceiling, and so we render from 1 block above the + // player (which is where the player's head is if they're 2 + // blocks tall) + 1 + } else { + // if the ceiling is a non-solid sky, use the largest value + // (subsequent arithmetic on ceiling_offset must be saturating) + i32::MAX + } + }) + }, + ) + .unwrap_or(0) + }; + if cpos.distance_squared(self.last_pos.xy()) >= 1 + || self.last_pos.z != pos.z as i32 + || self.last_ceiling != ceiling_offset + || new_chunks + { + self.last_pos = cpos.with_z(pos.z as i32); + self.last_ceiling = ceiling_offset; + for y in 0..VOXEL_MINIMAP_SIDELENGTH { + for x in 0..VOXEL_MINIMAP_SIDELENGTH { + let voff = Vec2::new(x as f32, y as f32); + let coff: Vec2 = voff + .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j)) + .as_(); + let cmod: Vec2 = voff + .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j)) + .as_(); + let column = self.chunk_minimaps.get(&(cpos + coff)); + let color: Rgba = column + .and_then(|column| { + let MinimapColumn { + zlo, + layers, + above, + below, + } = column; + if (pos.z as i32).saturating_add(ceiling_offset) < *zlo { + // If the ceiling is below the bottom of a chunk, color it black, + // so that the middles of caves/dungeons don't show the forests + // around them. + Some(Rgba::new(0, 0, 0, 255)) + } else { + // Otherwise, take the pixel from the precomputed z-level view at + // the ceiling's height (using the top slice of the chunk if the + // ceiling is above the chunk, (e.g. so that forests with + // differently-tall trees are handled properly) + layers + .get( + (((pos.z as i32 - zlo).saturating_add(ceiling_offset)) + as usize) + .min(layers.len().saturating_sub(1)), + ) + .and_then(|grid| grid.get(cmod).map(|c| c.0.as_())) + .or_else(|| { + Some(if pos.z as i32 > *zlo { + above.0 + } else { + below.0 + }) + }) + } + }) + .unwrap_or_else(Rgba::zero); + self.composited.put_pixel( + x, + VOXEL_MINIMAP_SIDELENGTH - y - 1, + image::Rgba([color.r, color.g, color.b, color.a]), + ); + } + } + + ui.replace_graphic( + self.image_id.none, + Graphic::Image( + Arc::new(DynamicImage::ImageRgba8(self.composited.clone())), + Some(Rgba::from([0.0, 0.0, 0.0, 0.0])), + ), + ); + } + } +} + widget_ids! { struct Ids { mmap_frame, @@ -40,6 +365,7 @@ widget_ids! { mmap_site_icons[], member_indicators[], location_marker, + voxel_minimap, } } @@ -56,6 +382,7 @@ pub struct MiniMap<'a> { ori: Vec3, global_state: &'a GlobalState, location_marker: Option>, + voxel_minimap: &'a VoxelMinimap, } impl<'a> MiniMap<'a> { @@ -69,6 +396,7 @@ impl<'a> MiniMap<'a> { ori: Vec3, global_state: &'a GlobalState, location_marker: Option>, + voxel_minimap: &'a VoxelMinimap, ) -> Self { Self { show, @@ -81,6 +409,7 @@ impl<'a> MiniMap<'a> { ori, global_state, location_marker, + voxel_minimap, } } } @@ -116,6 +445,7 @@ impl<'a> Widget for MiniMap<'a> { let show_minimap = self.global_state.settings.interface.minimap_show; let is_facing_north = self.global_state.settings.interface.minimap_face_north; let show_topo_map = self.global_state.settings.interface.map_show_topo_map; + let show_voxel_map = self.global_state.settings.interface.map_show_voxel_map; let orientation = if is_facing_north { Vec3::new(0.0, 1.0, 0.0) } else { @@ -277,6 +607,34 @@ impl<'a> Widget for MiniMap<'a> { .set(state.ids.map_layers[index], ui); } } + if show_voxel_map { + let voxelmap_rotation = if is_facing_north { + self.voxel_minimap.image_id.none + } else { + self.voxel_minimap.image_id.source_north + }; + let cmod: Vec2 = player_pos + .xy() + .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j)) + .as_(); + let rect_src = position::Rect::from_xy_dim( + [ + cmod.x + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0, + -cmod.y + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0, + ], + [ + TerrainChunkSize::RECT_SIZE.x as f64 * max_zoom / zoom, + TerrainChunkSize::RECT_SIZE.y as f64 * max_zoom / zoom, + ], + ); + Image::new(voxelmap_rotation) + .middle_of(state.ids.mmap_frame_bg) + .w_h(map_size.x, map_size.y) + .parent(state.ids.mmap_frame_bg) + .source_rectangle(rect_src) + .graphics_for(state.ids.map_layers[0]) + .set(state.ids.voxel_minimap, ui); + } // Map icons if state.ids.mmap_site_icons.len() < self.client.sites().len() { diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index bbc95ff3e9..434f1526bb 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -42,7 +42,7 @@ use img_ids::Imgs; use item_imgs::ItemImgs; use loot_scroller::LootScroller; use map::Map; -use minimap::MiniMap; +use minimap::{MiniMap, VoxelMinimap}; use popup::Popup; use prompt_dialog::PromptDialog; use serde::{Deserialize, Serialize}; @@ -81,6 +81,7 @@ use common::{ }, consts::MAX_PICKUP_RANGE, outcome::Outcome, + slowjob::SlowJobPool, terrain::{SpriteKind, TerrainChunk}, trade::{ReducedInventory, TradeAction}, uid::Uid, @@ -810,6 +811,7 @@ pub struct Hud { events: Vec, crosshair_opacity: f32, floaters: Floaters, + voxel_minimap: VoxelMinimap, } impl Hud { @@ -866,6 +868,7 @@ impl Hud { ); Self { + voxel_minimap: VoxelMinimap::new(&mut ui), ui, imgs, world_map, @@ -957,6 +960,9 @@ impl Hud { ) -> Vec { span!(_guard, "update_layout", "Hud::update_layout"); let mut events = core::mem::take(&mut self.events); + if global_state.settings.interface.map_show_voxel_map { + self.voxel_minimap.maintain(&client, &mut self.ui); + } let (ref mut ui_widgets, ref mut item_tooltip_manager, ref mut tooltip_manager) = &mut self.ui.set_widgets(); // self.ui.set_item_widgets(); pulse time for pulsating elements @@ -2364,6 +2370,7 @@ impl Hud { camera.get_orientation(), &global_state, self.show.location_marker, + &self.voxel_minimap, ) .set(self.ids.minimap, ui_widgets) { @@ -3599,9 +3606,14 @@ impl Hud { // Check if item images need to be reloaded self.item_imgs.reload_if_changed(&mut self.ui); - + // TODO: using a thread pool in the obvious way for speeding up map zoom results + // in flickering artifacts, figure out a better way to make use of the + // thread pool + let _pool = client.state().ecs().read_resource::(); self.ui.maintain( &mut global_state.window.renderer_mut(), + None, + //Some(&pool), Some(proj_mat * view_mat * Mat4::translation_3d(-focus_off)), ); diff --git a/voxygen/src/menu/char_selection/ui/mod.rs b/voxygen/src/menu/char_selection/ui/mod.rs index 502320c95d..0ce849a58e 100644 --- a/voxygen/src/menu/char_selection/ui/mod.rs +++ b/voxygen/src/menu/char_selection/ui/mod.rs @@ -1563,6 +1563,7 @@ impl CharSelectionUi { self.controls .view(&global_state.settings, &client, &self.error, &i18n), global_state.window.renderer_mut(), + None, global_state.clipboard.as_ref(), ); diff --git a/voxygen/src/menu/main/client_init.rs b/voxygen/src/menu/main/client_init.rs index de69abc5a5..6c213ffc0b 100644 --- a/voxygen/src/menu/main/client_init.rs +++ b/voxygen/src/menu/main/client_init.rs @@ -4,7 +4,7 @@ use client::{ Client, ServerInfo, }; use common::consts::MIN_RECOMMENDED_TOKIO_THREADS; -use crossbeam::channel::{unbounded, Receiver, Sender, TryRecvError}; +use crossbeam_channel::{unbounded, Receiver, Sender, TryRecvError}; use std::{ sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, diff --git a/voxygen/src/menu/main/ui/mod.rs b/voxygen/src/menu/main/ui/mod.rs index 063845266e..34de2acd5f 100644 --- a/voxygen/src/menu/main/ui/mod.rs +++ b/voxygen/src/menu/main/ui/mod.rs @@ -571,6 +571,7 @@ impl<'a> MainMenuUi { let (messages, _) = self.ui.maintain( self.controls.view(&global_state.settings, dt.as_secs_f32()), global_state.window.renderer_mut(), + None, global_state.clipboard.as_ref(), ); diff --git a/voxygen/src/scene/figure/cache.rs b/voxygen/src/scene/figure/cache.rs index c148d5a9dd..e74ba048a2 100644 --- a/voxygen/src/scene/figure/cache.rs +++ b/voxygen/src/scene/figure/cache.rs @@ -25,7 +25,7 @@ use common::{ vol::BaseVol, }; use core::{hash::Hash, ops::Range}; -use crossbeam::atomic; +use crossbeam_utils::atomic; use hashbrown::{hash_map::Entry, HashMap}; use std::sync::Arc; use vek::*; diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index 3713425bd5..65e34bde73 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -21,7 +21,7 @@ use common::{ }; use common_base::span; use core::{f32, fmt::Debug, i32, marker::PhantomData, time::Duration}; -use crossbeam::channel; +use crossbeam_channel as channel; use enum_iterator::IntoEnumIterator; use guillotiere::AtlasAllocator; use hashbrown::HashMap; diff --git a/voxygen/src/session/settings_change.rs b/voxygen/src/session/settings_change.rs index db4cc70b0e..a6d2f4c7c8 100644 --- a/voxygen/src/session/settings_change.rs +++ b/voxygen/src/session/settings_change.rs @@ -122,6 +122,7 @@ pub enum Interface { MapShowCaves(bool), MapShowTrees(bool), MapShowPeaks(bool), + MapShowVoxelMap(bool), ResetInterfaceSettings, } @@ -511,6 +512,9 @@ impl SettingsChange { Interface::MapShowPeaks(map_show_peaks) => { settings.interface.map_show_peaks = map_show_peaks; }, + Interface::MapShowVoxelMap(map_show_voxel_map) => { + settings.interface.map_show_voxel_map = map_show_voxel_map; + }, Interface::ResetInterfaceSettings => { // Reset Interface Settings let tmp = settings.interface.intro_show; diff --git a/voxygen/src/settings/interface.rs b/voxygen/src/settings/interface.rs index 4e898a9977..b3101b7245 100644 --- a/voxygen/src/settings/interface.rs +++ b/voxygen/src/settings/interface.rs @@ -34,6 +34,7 @@ pub struct InterfaceSettings { pub map_show_caves: bool, pub map_show_trees: bool, pub map_show_peaks: bool, + pub map_show_voxel_map: bool, pub minimap_show: bool, pub minimap_face_north: bool, pub minimap_zoom: f64, @@ -67,6 +68,7 @@ impl Default for InterfaceSettings { map_show_caves: true, map_show_trees: false, map_show_peaks: false, + map_show_voxel_map: false, minimap_show: true, minimap_face_north: false, minimap_zoom: 10.0, diff --git a/voxygen/src/singleplayer.rs b/voxygen/src/singleplayer.rs index 25c5575482..cb9fd50f20 100644 --- a/voxygen/src/singleplayer.rs +++ b/voxygen/src/singleplayer.rs @@ -1,5 +1,5 @@ use common::{clock::Clock, consts::MIN_RECOMMENDED_TOKIO_THREADS}; -use crossbeam::channel::{bounded, unbounded, Receiver, Sender, TryRecvError}; +use crossbeam_channel::{bounded, unbounded, Receiver, Sender, TryRecvError}; use server::{ persistence::{DatabaseSettings, SqlLogMode}, Error as ServerError, Event, Input, Server, diff --git a/voxygen/src/ui/graphic/mod.rs b/voxygen/src/ui/graphic/mod.rs index 1180b8bda4..241e14dbbc 100644 --- a/voxygen/src/ui/graphic/mod.rs +++ b/voxygen/src/ui/graphic/mod.rs @@ -3,13 +3,16 @@ mod renderer; pub use renderer::{SampleStrat, Transform}; -use crate::render::{RenderError, Renderer, Texture}; -use common::figure::Segment; +use crate::{ + render::{RenderError, Renderer, Texture}, + ui::KeyedJobs, +}; +use common::{figure::Segment, slowjob::SlowJobPool}; use guillotiere::{size2, SimpleAtlasAllocator}; use hashbrown::{hash_map::Entry, HashMap}; use image::{DynamicImage, RgbaImage}; use pixel_art::resize_pixel_art; -use std::sync::Arc; +use std::{hash::Hash, sync::Arc}; use tracing::warn; use vek::*; @@ -142,6 +145,9 @@ pub struct GraphicCache { textures: Vec, // Stores the location of graphics rendered at a particular resolution and cached on the cpu cache_map: HashMap, + + #[allow(clippy::type_complexity)] + keyed_jobs: KeyedJobs<(Id, Vec2), Option<(RgbaImage, Option>)>>, } impl GraphicCache { pub fn new(renderer: &mut Renderer) -> Self { @@ -153,6 +159,7 @@ impl GraphicCache { atlases: vec![(atlas, 0)], textures: vec![texture], cache_map: HashMap::default(), + keyed_jobs: KeyedJobs::new("IMAGE_PROCESSING"), } } @@ -222,6 +229,7 @@ impl GraphicCache { pub fn cache_res( &mut self, renderer: &mut Renderer, + pool: Option<&SlowJobPool>, graphic_id: Id, dims: Vec2, source: Aabr, @@ -277,7 +285,8 @@ impl GraphicCache { // graphic if !valid { // Create image - let (image, border) = draw_graphic(graphic_map, graphic_id, dims)?; + let (image, border) = + draw_graphic(graphic_map, graphic_id, dims, &mut self.keyed_jobs, pool)?; // If the cache location is invalid, we know the underlying texture is mutable, // so we should be able to replace the graphic. However, we still want to make // sure that we are not reusing textures for images that specify a border @@ -292,8 +301,10 @@ impl GraphicCache { Entry::Vacant(details) => details, }; - // Construct image - let (image, border_color) = draw_graphic(graphic_map, graphic_id, dims)?; + // Construct image in a threadpool + + let (image, border_color) = + draw_graphic(graphic_map, graphic_id, dims, &mut self.keyed_jobs, pool)?; // Upload let atlas_size = atlas_size(renderer); @@ -382,23 +393,43 @@ impl GraphicCache { } // Draw a graphic at the specified dimensions +#[allow(clippy::type_complexity)] fn draw_graphic( graphic_map: &GraphicMap, graphic_id: Id, dims: Vec2, + keyed_jobs: &mut KeyedJobs<(Id, Vec2), Option<(RgbaImage, Option>)>>, + pool: Option<&SlowJobPool>, ) -> Option<(RgbaImage, Option>)> { match graphic_map.get(&graphic_id) { + // Short-circuit spawning a job on the threadpool for blank graphics Some(Graphic::Blank) => None, - // Render image at requested resolution - // TODO: Use source aabr. - Some(&Graphic::Image(ref image, border_color)) => Some(( - resize_pixel_art(&image.to_rgba8(), u32::from(dims.x), u32::from(dims.y)), - border_color, - )), - Some(Graphic::Voxel(ref segment, trans, sample_strat)) => Some(( - renderer::draw_vox(&segment, dims, trans.clone(), *sample_strat), - None, - )), + Some(inner) => { + keyed_jobs + .spawn(pool, (graphic_id, dims), || { + let inner = inner.clone(); + move |_| { + match inner { + // Render image at requested resolution + // TODO: Use source aabr. + Graphic::Image(ref image, border_color) => Some(( + resize_pixel_art( + &image.to_rgba8(), + u32::from(dims.x), + u32::from(dims.y), + ), + border_color, + )), + Graphic::Voxel(ref segment, trans, sample_strat) => Some(( + renderer::draw_vox(&segment, dims, trans, sample_strat), + None, + )), + Graphic::Blank => None, + } + } + }) + .and_then(|(_, v)| v) + }, None => { warn!( ?graphic_id, diff --git a/voxygen/src/ui/ice/mod.rs b/voxygen/src/ui/ice/mod.rs index ea90a6048a..4103e1f562 100644 --- a/voxygen/src/ui/ice/mod.rs +++ b/voxygen/src/ui/ice/mod.rs @@ -15,6 +15,7 @@ use super::{ scale::{Scale, ScaleMode}, }; use crate::{render::Renderer, window::Window, Error}; +use common::slowjob::SlowJobPool; use common_base::span; use iced::{mouse, Cache, Size, UserInterface}; use iced_winit::Clipboard; @@ -142,6 +143,7 @@ impl IcedUi { &mut self, root: E, renderer: &mut Renderer, + pool: Option<&SlowJobPool>, clipboard: Option<&Clipboard>, ) -> (Vec, mouse::Interaction) { span!(_guard, "maintain", "IcedUi::maintain"); @@ -207,7 +209,7 @@ impl IcedUi { self.cache = Some(user_interface.into_cache()); - self.renderer.draw(primitive, renderer); + self.renderer.draw(primitive, renderer, pool); (messages, mouse_interaction) } diff --git a/voxygen/src/ui/ice/renderer/mod.rs b/voxygen/src/ui/ice/renderer/mod.rs index d6d819e767..bc7c29af9a 100644 --- a/voxygen/src/ui/ice/renderer/mod.rs +++ b/voxygen/src/ui/ice/renderer/mod.rs @@ -20,7 +20,7 @@ use crate::{ }, Error, }; -use common::util::srgba_to_linear; +use common::{slowjob::SlowJobPool, util::srgba_to_linear}; use common_base::span; use std::{convert::TryInto, ops::Range}; use vek::*; @@ -184,7 +184,12 @@ impl IcedRenderer { self.cache.resize_glyph_cache(renderer).unwrap(); } - pub fn draw(&mut self, primitive: Primitive, renderer: &mut Renderer) { + pub fn draw( + &mut self, + primitive: Primitive, + renderer: &mut Renderer, + pool: Option<&SlowJobPool>, + ) { span!(_guard, "draw", "IcedRenderer::draw"); // Re-use memory self.draw_commands.clear(); @@ -194,7 +199,7 @@ impl IcedRenderer { self.current_state = State::Plain; self.start = 0; - self.draw_primitive(primitive, Vec2::zero(), 1.0, renderer); + self.draw_primitive(primitive, Vec2::zero(), 1.0, renderer, pool); // Enter the final command. self.draw_commands.push(match self.current_state { @@ -426,12 +431,13 @@ impl IcedRenderer { offset: Vec2, alpha: f32, renderer: &mut Renderer, + pool: Option<&SlowJobPool>, ) { match primitive { Primitive::Group { primitives } => { primitives .into_iter() - .for_each(|p| self.draw_primitive(p, offset, alpha, renderer)); + .for_each(|p| self.draw_primitive(p, offset, alpha, renderer, pool)); }, Primitive::Image { handle, @@ -536,6 +542,7 @@ impl IcedRenderer { // Cache graphic at particular resolution. let (uv_aabr, tex_id) = match graphic_cache.cache_res( renderer, + pool, graphic_id, resolution, // TODO: take f32 here @@ -730,7 +737,7 @@ impl IcedRenderer { // TODO: cull primitives outside the current scissor // Renderer child - self.draw_primitive(*content, offset + clip_offset, alpha, renderer); + self.draw_primitive(*content, offset + clip_offset, alpha, renderer, pool); // Reset scissor self.draw_commands.push(match self.current_state { @@ -745,7 +752,7 @@ impl IcedRenderer { .push(DrawCommand::Scissor(self.window_scissor)); }, Primitive::Opacity { alpha: a, content } => { - self.draw_primitive(*content, offset, alpha * a, renderer); + self.draw_primitive(*content, offset, alpha * a, renderer, pool); }, Primitive::Nothing => {}, } diff --git a/voxygen/src/ui/keyed_jobs.rs b/voxygen/src/ui/keyed_jobs.rs new file mode 100644 index 0000000000..72f61b33b7 --- /dev/null +++ b/voxygen/src/ui/keyed_jobs.rs @@ -0,0 +1,102 @@ +use common::slowjob::{SlowJob, SlowJobPool}; +use hashbrown::{hash_map::Entry, HashMap}; +use std::{ + hash::Hash, + time::{Duration, Instant}, +}; + +enum KeyedJobTask { + Pending(Instant, Option), + Completed(Instant, V), +} + +pub struct KeyedJobs { + tx: crossbeam_channel::Sender<(K, V)>, + rx: crossbeam_channel::Receiver<(K, V)>, + tasks: HashMap>, + name: &'static str, + last_gc: Instant, +} + +const KEYEDJOBS_GC_INTERVAL: Duration = Duration::from_secs(1); + +impl KeyedJobs { + #[allow(clippy::new_without_default)] + pub fn new(name: &'static str) -> Self { + let (tx, rx) = crossbeam_channel::unbounded(); + Self { + tx, + rx, + tasks: HashMap::new(), + name, + last_gc: Instant::now(), + } + } + + /// Spawn a task on a specified threadpool. The function is given as a thunk + /// so that if work is needed to create captured variables (e.g. + /// `Arc::clone`), that only occurs if the task hasn't yet been scheduled. + pub fn spawn V + Send + Sync + 'static>( + &mut self, + pool: Option<&SlowJobPool>, + k: K, + f: impl FnOnce() -> F, + ) -> Option<(K, V)> { + if let Some(pool) = pool { + while let Ok((k2, v)) = self.rx.try_recv() { + if k == k2 { + return Some((k, v)); + } else { + self.tasks + .insert(k2, KeyedJobTask::Completed(Instant::now(), v)); + } + } + let now = Instant::now(); + if now - self.last_gc > KEYEDJOBS_GC_INTERVAL { + self.last_gc = now; + self.tasks.retain(|_, task| match task { + KeyedJobTask::Completed(at, _) => now - *at < KEYEDJOBS_GC_INTERVAL, + KeyedJobTask::Pending(at, job) => { + let fresh = now - *at < KEYEDJOBS_GC_INTERVAL; + if !fresh { + if let Some(job) = job.take() { + pool.cancel(job) + } + } + fresh + }, + }); + } + match self.tasks.entry(k.clone()) { + Entry::Occupied(e) => { + let mut ret = None; + e.replace_entry_with(|_, v| { + if let KeyedJobTask::Completed(_, v) = v { + ret = Some((k, v)); + None + } else { + Some(v) + } + }); + ret + }, + Entry::Vacant(e) => { + // TODO: consider adding a limit to the number of submitted jobs based on the + // number of available threads, once SlowJobPool supports a notion of + // approximating that + let tx = self.tx.clone(); + let f = f(); + let job = pool.spawn(self.name, move || { + let v = f(&k); + let _ = tx.send((k, v)); + }); + e.insert(KeyedJobTask::Pending(Instant::now(), Some(job))); + None + }, + } + } else { + let v = f()(&k); + Some((k, v)) + } + } +} diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index 64ad6ec49d..9a4ea66dc2 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -8,9 +8,11 @@ pub mod img_ids; #[macro_use] pub mod fonts; pub mod ice; +pub mod keyed_jobs; pub use event::Event; pub use graphic::{Graphic, Id as GraphicId, Rotation, SampleStrat, Transform}; +pub use keyed_jobs::KeyedJobs; pub use scale::{Scale, ScaleMode}; pub use widgets::{ image_frame::ImageFrame, @@ -34,7 +36,7 @@ use crate::{ #[rustfmt::skip] use ::image::GenericImageView; use cache::Cache; -use common::util::srgba_to_linear; +use common::{slowjob::SlowJobPool, util::srgba_to_linear}; use common_base::span; use conrod_core::{ event::Input, @@ -306,7 +308,12 @@ impl Ui { pub fn widget_input(&self, id: widget::Id) -> Widget { self.ui.widget_input(id) } #[allow(clippy::float_cmp)] // TODO: Pending review in #587 - pub fn maintain(&mut self, renderer: &mut Renderer, view_projection_mat: Option>) { + pub fn maintain( + &mut self, + renderer: &mut Renderer, + pool: Option<&SlowJobPool>, + view_projection_mat: Option>, + ) { span!(_guard, "maintain", "Ui::maintain"); // Maintain tooltip manager self.tooltip_manager @@ -353,16 +360,17 @@ impl Ui { } let mut retry = false; - self.maintain_internal(renderer, view_projection_mat, &mut retry); + self.maintain_internal(renderer, pool, view_projection_mat, &mut retry); if retry { // Update the glyph cache and try again. - self.maintain_internal(renderer, view_projection_mat, &mut retry); + self.maintain_internal(renderer, pool, view_projection_mat, &mut retry); } } fn maintain_internal( &mut self, renderer: &mut Renderer, + pool: Option<&SlowJobPool>, view_projection_mat: Option>, retry: &mut bool, ) { @@ -806,6 +814,7 @@ impl Ui { // Cache graphic at particular resolution. let (uv_aabr, tex_id) = match graphic_cache.cache_res( renderer, + pool, *graphic_id, resolution, source_aabr, diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index c4e37e9c61..06179e860a 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -5,7 +5,7 @@ use crate::{ ui, Error, }; use common_base::span; -use crossbeam::channel; +use crossbeam_channel as channel; use gilrs::{EventType, Gilrs}; use hashbrown::HashMap; use itertools::Itertools;