From 7980230b7f244a153f4264dd3f6e420149017ee4 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Mon, 10 May 2021 23:09:14 -0400 Subject: [PATCH 01/16] Add a minimap overlay based on voxel data. --- Cargo.lock | 11 +++- voxygen/Cargo.toml | 2 +- voxygen/src/hud/minimap.rs | 131 ++++++++++++++++++++++++++++++++++++- voxygen/src/hud/mod.rs | 6 +- 4 files changed, 145 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3fb5eb69dd..249ae8bd2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2415,6 +2415,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inline_tweak" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7033e97b20277cc0d043226d1940fa7719ff08d2305d1fc7421e53066d00eb4b" +dependencies = [ + "lazy_static", +] + [[package]] name = "inotify" version = "0.7.1" @@ -5650,7 +5659,6 @@ dependencies = [ name = "veloren-i18n" version = "0.9.0" dependencies = [ - "clap", "deunicode", "git2", "hashbrown", @@ -5833,6 +5841,7 @@ dependencies = [ "iced_native", "iced_winit", "image", + "inline_tweak", "itertools 0.10.0", "keyboard-keynames", "lazy_static", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index dfd6c62396..6bc04cbe06 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -104,7 +104,7 @@ treeculler = "0.2" tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] } num_cpus = "1.0" # vec_map = { version = "0.8.2" } -# inline_tweak = "1.0.2" +inline_tweak = "1.0.2" itertools = "0.10.0" # Tracy diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 0fd79bdca8..3aa3ca502b 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -4,22 +4,113 @@ 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}, GlobalState, }; use client::{self, Client}; -use common::{comp, comp::group::Role, terrain::TerrainChunkSize, vol::RectVolSize}; +use common::{ + comp, + comp::group::Role, + grid::Grid, + terrain::TerrainChunkSize, + 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::*; +pub struct VoxelMinimap { + chunk_minimaps: HashMap, HashMap>>, + composited: RgbaImage, + image_id: img_ids::Rotations, +} + +impl VoxelMinimap { + pub fn new(ui: &mut Ui) -> Self { + let mut composited = RgbaImage::new(96, 96); + for x in 0..96 { + for y in 0..96 { + composited.put_pixel( + x, + y, + image::Rgba([255 - 2 * x as u8, 255 - 2 * y as u8, 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, + } + } + + pub fn maintain(&mut self, client: &Client, ui: &mut Ui) { + let terrain = client.state().terrain(); + for (key, chunk) in terrain.iter() { + if !self.chunk_minimaps.contains_key(&key) { + let mut layers = HashMap::new(); + for z in chunk.get_min_z()..chunk.get_max_z() { + let grid = Grid::populate_from(Vec2::new(32, 32), |v| { + chunk + .get(Vec3::new(v.x, v.y, z)) + .ok() + .and_then(|block| block.get_color()) + .map(|rgb| [rgb.r, rgb.g, rgb.b, 128]) + .unwrap_or([0, 0, 0, 0]) + }); + layers.insert(z, grid); + } + self.chunk_minimaps.insert(key, layers); + } + } + let player = client.entity(); + if let Some(pos) = client.state().ecs().read_storage::().get(player) { + let pos = pos.0; + let cpos: Vec2 = (pos.xy() / 32.0).as_(); + for i in -1..=1 { + for j in -1..=1 { + let coff = Vec2::new(i, j); + if let Some(grid) = self + .chunk_minimaps + .get(&(cpos + coff)) + .and_then(|l| l.get(&(pos.z as i32))) + { + for x in 0..32 { + for y in 0..32 { + self.composited.put_pixel( + (i + 1) as u32 * 32 + x, + (j + 1) as u32 * 32 + y, + grid.get(Vec2::new(x, y).as_()) + .map(|c| image::Rgba(*c)) + .unwrap_or(image::Rgba([0, 0, 0, 0])), + ); + } + } + } + } + } + // TODO: don't leak memory, replace + self.image_id = ui.add_graphic_with_rotations(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 +131,7 @@ widget_ids! { mmap_site_icons[], member_indicators[], location_marker, + voxel_minimap, } } @@ -56,6 +148,7 @@ pub struct MiniMap<'a> { ori: Vec3, global_state: &'a GlobalState, location_marker: Option>, + voxel_minimap: &'a VoxelMinimap, } impl<'a> MiniMap<'a> { @@ -69,6 +162,7 @@ impl<'a> MiniMap<'a> { ori: Vec3, global_state: &'a GlobalState, location_marker: Option>, + voxel_minimap: &'a VoxelMinimap, ) -> Self { Self { show, @@ -81,6 +175,7 @@ impl<'a> MiniMap<'a> { ori, global_state, location_marker, + voxel_minimap, } } } @@ -116,6 +211,8 @@ 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 show_voxel_map = true; let orientation = if is_facing_north { Vec3::new(0.0, 1.0, 0.0) } else { @@ -277,6 +374,36 @@ 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 + }; + use inline_tweak::tweak; + /*let rect_src = position::Rect::from_xy_dim( + [ + player_pos.x as f64 / TerrainChunkSize::RECT_SIZE.x as f64 + tweak!(0.0), + worldsize.y as f64 - + (player_pos.y as f64 / TerrainChunkSize::RECT_SIZE.y as f64) + tweak!(0.0), + ], + [w_src / tweak!(32768.0), h_src / tweak!(32768.0)], + );*/ + let rect_src = position::Rect::from_xy_dim([tweak!(48.0), tweak!(48.0)], [ + tweak!(96.0), + tweak!(96.0), + ]); + Image::new(voxelmap_rotation) + .middle_of(state.ids.mmap_frame_bg) + .w_h( + map_size.x * 3.0 * (zoom / max_zoom), + map_size.y * 3.0 * zoom / max_zoom, + ) + .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..867c9434c0 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}; @@ -810,6 +810,7 @@ pub struct Hud { events: Vec, crosshair_opacity: f32, floaters: Floaters, + voxel_minimap: VoxelMinimap, } impl Hud { @@ -866,6 +867,7 @@ impl Hud { ); Self { + voxel_minimap: VoxelMinimap::new(&mut ui), ui, imgs, world_map, @@ -957,6 +959,7 @@ impl Hud { ) -> Vec { span!(_guard, "update_layout", "Hud::update_layout"); let mut events = core::mem::take(&mut self.events); + 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 +2367,7 @@ impl Hud { camera.get_orientation(), &global_state, self.show.location_marker, + &self.voxel_minimap, ) .set(self.ids.minimap, ui_widgets) { From 937815d8c38e015f0b9a0facbf6e8b2277f9ce03 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Tue, 11 May 2021 01:08:03 -0400 Subject: [PATCH 02/16] Make all the offsets for voxel minimap work properly for arbitrary sizes, and fix the memory leak. --- Cargo.lock | 11 +----- voxygen/Cargo.toml | 2 +- voxygen/src/hud/minimap.rs | 79 ++++++++++++++++---------------------- 3 files changed, 36 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 249ae8bd2e..3fb5eb69dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2415,15 +2415,6 @@ dependencies = [ "serde", ] -[[package]] -name = "inline_tweak" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7033e97b20277cc0d043226d1940fa7719ff08d2305d1fc7421e53066d00eb4b" -dependencies = [ - "lazy_static", -] - [[package]] name = "inotify" version = "0.7.1" @@ -5659,6 +5650,7 @@ dependencies = [ name = "veloren-i18n" version = "0.9.0" dependencies = [ + "clap", "deunicode", "git2", "hashbrown", @@ -5841,7 +5833,6 @@ dependencies = [ "iced_native", "iced_winit", "image", - "inline_tweak", "itertools 0.10.0", "keyboard-keynames", "lazy_static", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 6bc04cbe06..dfd6c62396 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -104,7 +104,7 @@ treeculler = "0.2" tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] } num_cpus = "1.0" # vec_map = { version = "0.8.2" } -inline_tweak = "1.0.2" +# inline_tweak = "1.0.2" itertools = "0.10.0" # Tracy diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 3aa3ca502b..8c5afaa3f4 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -35,16 +35,13 @@ pub struct VoxelMinimap { image_id: img_ids::Rotations, } +const VOXEL_MINIMAP_SIDELENGTH: u32 = 512; impl VoxelMinimap { pub fn new(ui: &mut Ui) -> Self { - let mut composited = RgbaImage::new(96, 96); - for x in 0..96 { - for y in 0..96 { - composited.put_pixel( - x, - y, - image::Rgba([255 - 2 * x as u8, 255 - 2 * y as u8, 0, 64]), - ); + let mut composited = RgbaImage::new(VOXEL_MINIMAP_SIDELENGTH, VOXEL_MINIMAP_SIDELENGTH); + for x in 0..VOXEL_MINIMAP_SIDELENGTH { + for y in 0..VOXEL_MINIMAP_SIDELENGTH { + composited.put_pixel(x, y, image::Rgba([0, 0, 0, 64])); } } Self { @@ -68,7 +65,7 @@ impl VoxelMinimap { .get(Vec3::new(v.x, v.y, z)) .ok() .and_then(|block| block.get_color()) - .map(|rgb| [rgb.r, rgb.g, rgb.b, 128]) + .map(|rgb| [rgb.r, rgb.g, rgb.b, 192]) .unwrap_or([0, 0, 0, 0]) }); layers.insert(z, grid); @@ -79,34 +76,34 @@ impl VoxelMinimap { let player = client.entity(); if let Some(pos) = client.state().ecs().read_storage::().get(player) { let pos = pos.0; - let cpos: Vec2 = (pos.xy() / 32.0).as_(); - for i in -1..=1 { - for j in -1..=1 { - let coff = Vec2::new(i, j); + for x in 0..VOXEL_MINIMAP_SIDELENGTH { + for y in 0..VOXEL_MINIMAP_SIDELENGTH { + let vpos = pos.xy() + Vec2::new(x as f32, y as f32) + - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0; + let cpos: Vec2 = (vpos / 32.0).as_(); + let cmod: Vec2 = (vpos % 32.0).as_(); if let Some(grid) = self .chunk_minimaps - .get(&(cpos + coff)) + .get(&cpos) .and_then(|l| l.get(&(pos.z as i32))) { - for x in 0..32 { - for y in 0..32 { - self.composited.put_pixel( - (i + 1) as u32 * 32 + x, - (j + 1) as u32 * 32 + y, - grid.get(Vec2::new(x, y).as_()) - .map(|c| image::Rgba(*c)) - .unwrap_or(image::Rgba([0, 0, 0, 0])), - ); - } - } + self.composited.put_pixel( + x, + VOXEL_MINIMAP_SIDELENGTH - y - 1, + grid.get(cmod) + .map(|c| image::Rgba(*c)) + .unwrap_or(image::Rgba([0, 0, 0, 0])), + ); } } } - // TODO: don't leak memory, replace - self.image_id = ui.add_graphic_with_rotations(Graphic::Image( - Arc::new(DynamicImage::ImageRgba8(self.composited.clone())), - Some(Rgba::from([0.0, 0.0, 0.0, 0.0])), - )); + 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])), + ), + ); } } } @@ -380,25 +377,17 @@ impl<'a> Widget for MiniMap<'a> { } else { self.voxel_minimap.image_id.source_north }; - use inline_tweak::tweak; - /*let rect_src = position::Rect::from_xy_dim( + let scaling = (VOXEL_MINIMAP_SIDELENGTH as f64 / 32.0) * max_zoom / zoom; + let rect_src = position::Rect::from_xy_dim( [ - player_pos.x as f64 / TerrainChunkSize::RECT_SIZE.x as f64 + tweak!(0.0), - worldsize.y as f64 - - (player_pos.y as f64 / TerrainChunkSize::RECT_SIZE.y as f64) + tweak!(0.0), + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0, + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0, ], - [w_src / tweak!(32768.0), h_src / tweak!(32768.0)], - );*/ - let rect_src = position::Rect::from_xy_dim([tweak!(48.0), tweak!(48.0)], [ - tweak!(96.0), - tweak!(96.0), - ]); + [scaling, scaling], + ); Image::new(voxelmap_rotation) .middle_of(state.ids.mmap_frame_bg) - .w_h( - map_size.x * 3.0 * (zoom / max_zoom), - map_size.y * 3.0 * zoom / max_zoom, - ) + .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]) From 6df2e96d25b754e871da29ab6b4ab5911ebfe47a Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Tue, 11 May 2021 17:43:45 -0400 Subject: [PATCH 03/16] Improve the efficiency of the voxel minimap by only updating it when crossing a chunk boundary or changing z-level. Allows making the 9 chunks nearest to the player fancier by compositing multiple z levels. --- Cargo.lock | 10 +++++ voxygen/Cargo.toml | 2 +- voxygen/src/hud/minimap.rs | 85 ++++++++++++++++++++++++-------------- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3fb5eb69dd..ebe9001f23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2415,6 +2415,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inline_tweak" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7033e97b20277cc0d043226d1940fa7719ff08d2305d1fc7421e53066d00eb4b" +dependencies = [ + "lazy_static", +] + [[package]] name = "inotify" version = "0.7.1" @@ -5833,6 +5842,7 @@ dependencies = [ "iced_native", "iced_winit", "image", + "inline_tweak", "itertools 0.10.0", "keyboard-keynames", "lazy_static", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index dfd6c62396..6bc04cbe06 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -104,7 +104,7 @@ treeculler = "0.2" tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] } num_cpus = "1.0" # vec_map = { version = "0.8.2" } -# inline_tweak = "1.0.2" +inline_tweak = "1.0.2" itertools = "0.10.0" # Tracy diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 8c5afaa3f4..149f476212 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -30,9 +30,10 @@ use std::sync::Arc; use vek::*; pub struct VoxelMinimap { - chunk_minimaps: HashMap, HashMap>>, + chunk_minimaps: HashMap, (i32, Vec>)>, composited: RgbaImage, image_id: img_ids::Rotations, + last_pos: Vec3, } const VOXEL_MINIMAP_SIDELENGTH: u32 = 512; @@ -51,6 +52,7 @@ impl VoxelMinimap { Some(Rgba::from([0.0, 0.0, 0.0, 0.0])), )), composited, + last_pos: Vec3::zero(), } } @@ -58,7 +60,7 @@ impl VoxelMinimap { let terrain = client.state().terrain(); for (key, chunk) in terrain.iter() { if !self.chunk_minimaps.contains_key(&key) { - let mut layers = HashMap::new(); + let mut layers = Vec::new(); for z in chunk.get_min_z()..chunk.get_max_z() { let grid = Grid::populate_from(Vec2::new(32, 32), |v| { chunk @@ -68,42 +70,64 @@ impl VoxelMinimap { .map(|rgb| [rgb.r, rgb.g, rgb.b, 192]) .unwrap_or([0, 0, 0, 0]) }); - layers.insert(z, grid); + layers.push(grid); } - self.chunk_minimaps.insert(key, layers); + self.chunk_minimaps.insert(key, (chunk.get_min_z(), layers)); } } let player = client.entity(); if let Some(pos) = client.state().ecs().read_storage::().get(player) { let pos = pos.0; - for x in 0..VOXEL_MINIMAP_SIDELENGTH { + let vpos = pos.xy() - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0; + let cpos: Vec2 = vpos.map(|i| (i as i32).div_euclid(32)); + if cpos.distance_squared(self.last_pos.xy()) >= 1 || self.last_pos.z != pos.z as i32 { + self.last_pos = cpos.with_z(pos.z as i32); for y in 0..VOXEL_MINIMAP_SIDELENGTH { - let vpos = pos.xy() + Vec2::new(x as f32, y as f32) - - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0; - let cpos: Vec2 = (vpos / 32.0).as_(); - let cmod: Vec2 = (vpos % 32.0).as_(); - if let Some(grid) = self - .chunk_minimaps - .get(&cpos) - .and_then(|l| l.get(&(pos.z as i32))) - { - self.composited.put_pixel( - x, - VOXEL_MINIMAP_SIDELENGTH - y - 1, - grid.get(cmod) - .map(|c| image::Rgba(*c)) - .unwrap_or(image::Rgba([0, 0, 0, 0])), - ); + for x in 0..VOXEL_MINIMAP_SIDELENGTH { + let voff = Vec2::new(x as f32, y as f32); + let coff: Vec2 = voff.map(|i| (i as i32).div_euclid(32)); + let cmod: Vec2 = voff.map(|i| (i as i32).rem_euclid(32)); + let mut rgba = Vec4::::zero(); + let (weights, zoff) = + if (x as i32 - VOXEL_MINIMAP_SIDELENGTH as i32 / 2).abs() < 96 + && (y as i32 - VOXEL_MINIMAP_SIDELENGTH as i32 / 2).abs() < 96 + { + (&[2, 4, 1, 1, 1][..], -1) + } else { + (&[1][..], 0) + }; + for z in 0..weights.len() { + if let Some(grid) = + self.chunk_minimaps + .get(&(cpos + coff)) + .and_then(|(zlo, g)| { + g.get((pos.z as i32 + z as i32 - zlo + zoff) as usize) + }) + { + let tmp: Vec4 = grid + .get(cmod) + .map(|c| Vec4::::from(*c).as_()) + .unwrap_or(Vec4::one()); + rgba += tmp.as_() * weights[z]; + } + } + let color = { + let rgba: Vec4 = (rgba / weights.iter().sum::()).as_(); + image::Rgba([rgba.x, rgba.y, rgba.z, rgba.w]) + }; + self.composited + .put_pixel(x, VOXEL_MINIMAP_SIDELENGTH - y - 1, color); } } + + 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])), + ), + ); } - 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])), - ), - ); } } } @@ -378,10 +402,11 @@ impl<'a> Widget for MiniMap<'a> { self.voxel_minimap.image_id.source_north }; let scaling = (VOXEL_MINIMAP_SIDELENGTH as f64 / 32.0) * max_zoom / zoom; + let cmod: Vec2 = (player_pos.xy() % 32.0).as_(); let rect_src = position::Rect::from_xy_dim( [ - VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0, - VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0, + cmod.x + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0, + -cmod.y + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0, ], [scaling, scaling], ); From d878a2050ad46664cf7c5db458ecab175361c028 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 13 May 2021 01:32:24 -0400 Subject: [PATCH 04/16] Composite the layers on chunk load, and try to add an overhead mode. --- voxygen/src/hud/minimap.rs | 135 ++++++++++++++++++++++++++----------- 1 file changed, 94 insertions(+), 41 deletions(-) diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 149f476212..b547374df5 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -14,7 +14,7 @@ use common::{ comp, comp::group::Role, grid::Grid, - terrain::TerrainChunkSize, + terrain::{Block, TerrainChunk, TerrainChunkSize}, vol::{ReadVol, RectVolSize}, }; use common_net::msg::world_msg::SiteKind; @@ -30,7 +30,7 @@ use std::sync::Arc; use vek::*; pub struct VoxelMinimap { - chunk_minimaps: HashMap, (i32, Vec>)>, + chunk_minimaps: HashMap, (i32, Vec, bool)>>)>, composited: RgbaImage, image_id: img_ids::Rotations, last_pos: Vec3, @@ -56,21 +56,76 @@ impl VoxelMinimap { } } + fn block_color(block: &Block) -> Option> { + block + .get_color() + .map(|rgb| Vec4::new(rgb.r, rgb.g, rgb.b, 192)) + } + + /// 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 = Vec4::::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(Vec4::zero()); + rgba += color.as_() * weights[dz as usize] as f32; + } + let rgba: Vec4 = (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_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.map(|j| { + (j as u32).saturating_sub(if seen_air > 2 { 4 } else { 0 }) as u8 + })); + break; + } + } else { + seen_air += 1; + } + } + let is_filled = chunk + .get(Vec3::new(v.x, v.y, z)) + .ok() + .map_or(true, |b| b.is_filled()); + (rgba.unwrap_or_else(Vec4::zero), is_filled) + }); + layers.push(grid); + } + } + pub fn maintain(&mut self, client: &Client, ui: &mut Ui) { let terrain = client.state().terrain(); for (key, chunk) in terrain.iter() { if !self.chunk_minimaps.contains_key(&key) { let mut layers = Vec::new(); - for z in chunk.get_min_z()..chunk.get_max_z() { - let grid = Grid::populate_from(Vec2::new(32, 32), |v| { - chunk - .get(Vec3::new(v.x, v.y, z)) - .ok() - .and_then(|block| block.get_color()) - .map(|rgb| [rgb.r, rgb.g, rgb.b, 192]) - .unwrap_or([0, 0, 0, 0]) - }); - layers.push(grid); + const MODE_OVERHEAD: bool = true; + if MODE_OVERHEAD { + Self::composite_layer_overhead(chunk, &mut layers); + } else { + Self::composite_layer_slice(chunk, &mut layers); } self.chunk_minimaps.insert(key, (chunk.get_min_z(), layers)); } @@ -87,36 +142,34 @@ impl VoxelMinimap { let voff = Vec2::new(x as f32, y as f32); let coff: Vec2 = voff.map(|i| (i as i32).div_euclid(32)); let cmod: Vec2 = voff.map(|i| (i as i32).rem_euclid(32)); - let mut rgba = Vec4::::zero(); - let (weights, zoff) = - if (x as i32 - VOXEL_MINIMAP_SIDELENGTH as i32 / 2).abs() < 96 - && (y as i32 - VOXEL_MINIMAP_SIDELENGTH as i32 / 2).abs() < 96 - { - (&[2, 4, 1, 1, 1][..], -1) - } else { - (&[1][..], 0) - }; - for z in 0..weights.len() { - if let Some(grid) = - self.chunk_minimaps - .get(&(cpos + coff)) - .and_then(|(zlo, g)| { - g.get((pos.z as i32 + z as i32 - zlo + zoff) as usize) + let column = self.chunk_minimaps.get(&(cpos + coff)); + /*let ceiling_offset = column + .and_then(|(zlo, g)| { + (0..64) + .filter_map(|dz| { + g.get((pos.z as i32 - zlo + dz) as usize).and_then(|grid| { + if grid.get(cmod).map_or(false, |(_, b)| *b) { + Some(dz) + } else { + None + } + }) }) - { - let tmp: Vec4 = grid - .get(cmod) - .map(|c| Vec4::::from(*c).as_()) - .unwrap_or(Vec4::one()); - rgba += tmp.as_() * weights[z]; - } - } - let color = { - let rgba: Vec4 = (rgba / weights.iter().sum::()).as_(); - image::Rgba([rgba.x, rgba.y, rgba.z, rgba.w]) - }; - self.composited - .put_pixel(x, VOXEL_MINIMAP_SIDELENGTH - y - 1, color); + .next() + }) + .unwrap_or(0);*/ + let ceiling_offset = 8; + let color: Vec4 = column + .and_then(|(zlo, g)| { + g.get((pos.z as i32 - zlo + ceiling_offset) as usize) + }) + .and_then(|grid| grid.get(cmod).map(|c| c.0.as_())) + .unwrap_or(Vec4::zero()); + self.composited.put_pixel( + x, + VOXEL_MINIMAP_SIDELENGTH - y - 1, + image::Rgba([color.x, color.y, color.z, color.w]), + ); } } From 59e93d23c1c7fc5bb7d1eef789264f39ce7307cf Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 13 May 2021 11:44:30 -0400 Subject: [PATCH 05/16] Improve ceiling detection for the voxel minimap. --- voxygen/src/hud/minimap.rs | 135 ++++++++++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 23 deletions(-) diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index b547374df5..1c4dc5e8bb 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -29,11 +29,23 @@ 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: (Vec4, bool), + /// Color and filledness below the lowest layer + below: (Vec4, bool), +} + pub struct VoxelMinimap { - chunk_minimaps: HashMap, (i32, Vec, bool)>>)>, + chunk_minimaps: HashMap, MinimapColumn>, composited: RgbaImage, image_id: img_ids::Rotations, last_pos: Vec3, + last_ceiling: i32, } const VOXEL_MINIMAP_SIDELENGTH: u32 = 512; @@ -53,6 +65,7 @@ impl VoxelMinimap { )), composited, last_pos: Vec3::zero(), + last_ceiling: 0, } } @@ -89,6 +102,7 @@ impl VoxelMinimap { 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 @@ -102,9 +116,15 @@ impl VoxelMinimap { })); 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 is_filled = chunk .get(Vec3::new(v.x, v.y, z)) @@ -118,8 +138,10 @@ impl VoxelMinimap { pub fn maintain(&mut self, client: &Client, ui: &mut Ui) { let terrain = client.state().terrain(); + let mut new_chunk = false; for (key, chunk) in terrain.iter() { if !self.chunk_minimaps.contains_key(&key) { + new_chunk = true; let mut layers = Vec::new(); const MODE_OVERHEAD: bool = true; if MODE_OVERHEAD { @@ -127,7 +149,28 @@ impl VoxelMinimap { } else { Self::composite_layer_slice(chunk, &mut layers); } - self.chunk_minimaps.insert(key, (chunk.get_min_z(), layers)); + let above = chunk + .get(Vec3::new(0, 0, chunk.get_max_z() + 1)) + .ok() + .cloned() + .unwrap_or_else(Block::empty); + let below = chunk + .get(Vec3::new(0, 0, chunk.get_min_z() - 1)) + .ok() + .cloned() + .unwrap_or_else(Block::empty); + self.chunk_minimaps.insert(key, MinimapColumn { + zlo: chunk.get_min_z(), + layers, + above: ( + Self::block_color(&above).unwrap_or_else(Vec4::zero), + above.is_filled(), + ), + below: ( + Self::block_color(&below).unwrap_or_else(Vec4::zero), + below.is_filled(), + ), + }); } } let player = client.entity(); @@ -135,35 +178,81 @@ impl VoxelMinimap { let pos = pos.0; let vpos = pos.xy() - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0; let cpos: Vec2 = vpos.map(|i| (i as i32).div_euclid(32)); - if cpos.distance_squared(self.last_pos.xy()) >= 1 || self.last_pos.z != pos.z as i32 { + let ceiling_offset = { + let voff = Vec2::new( + VOXEL_MINIMAP_SIDELENGTH as f32, + VOXEL_MINIMAP_SIDELENGTH as f32, + ) / 2.0; + let coff: Vec2 = voff.map(|i| (i as i32).div_euclid(32)); + let cmod: Vec2 = vpos.map(|i| (i as i32).rem_euclid(32)); + let column = self.chunk_minimaps.get(&(cpos + coff)); + column + .map( + |MinimapColumn { + zlo, layers, above, .. + }| { + (0..layers.len() as i32) + .filter_map(|dz| { + layers.get((pos.z as i32 - zlo + dz) as usize).and_then( + |grid| { + if grid.get(cmod).map_or(false, |(_, b)| *b) { + Some(dz) + } else { + None + } + }, + ) + }) + .next() + .unwrap_or_else(|| { + if above.1 { + 1 + } else { + layers.len() as i32 - pos.z as i32 + zlo + } + }) + }, + ) + .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_chunk + { + tracing::info!("{:?} {:?}", pos, ceiling_offset); 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.map(|i| (i as i32).div_euclid(32)); let cmod: Vec2 = voff.map(|i| (i as i32).rem_euclid(32)); let column = self.chunk_minimaps.get(&(cpos + coff)); - /*let ceiling_offset = column - .and_then(|(zlo, g)| { - (0..64) - .filter_map(|dz| { - g.get((pos.z as i32 - zlo + dz) as usize).and_then(|grid| { - if grid.get(cmod).map_or(false, |(_, b)| *b) { - Some(dz) - } else { - None - } - }) - }) - .next() - }) - .unwrap_or(0);*/ - let ceiling_offset = 8; + //let ceiling_offset = 8; let color: Vec4 = column - .and_then(|(zlo, g)| { - g.get((pos.z as i32 - zlo + ceiling_offset) as usize) - }) - .and_then(|grid| grid.get(cmod).map(|c| c.0.as_())) + .and_then( + |MinimapColumn { + zlo, + layers, + above, + below, + }| { + layers + .get( + ((pos.z as i32 - zlo + ceiling_offset) as usize) + .min(layers.len() - 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(Vec4::zero()); self.composited.put_pixel( x, From 1214715c212039a791e21eeebd174400e28e5c5b Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 13 May 2021 18:24:11 -0400 Subject: [PATCH 06/16] Use a threadpool to speed up minimap chunk rendering. Also fix ceiling height calculations and color water blue. --- Cargo.lock | 1 + voxygen/Cargo.toml | 1 + voxygen/src/ecs/mod.rs | 1 + voxygen/src/hud/minimap.rs | 98 ++++++++++++++--------- voxygen/src/hud/mod.rs | 8 +- voxygen/src/menu/char_selection/ui/mod.rs | 1 + voxygen/src/menu/main/ui/mod.rs | 1 + voxygen/src/ui/graphic/mod.rs | 60 ++++++++++---- voxygen/src/ui/ice/mod.rs | 4 +- voxygen/src/ui/ice/renderer/mod.rs | 19 +++-- voxygen/src/ui/mod.rs | 67 ++++++++++++++-- 11 files changed, 192 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ebe9001f23..17d6ebc8b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5825,6 +5825,7 @@ dependencies = [ "cpal", "criterion", "crossbeam", + "crossbeam-channel", "directories-next", "dispatch 0.1.4", "dot_vox", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 6bc04cbe06..1cd3a85aba 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -106,6 +106,7 @@ num_cpus = "1.0" # vec_map = { version = "0.8.2" } inline_tweak = "1.0.2" itertools = "0.10.0" +crossbeam-channel = "0.5" # Tracy tracing = "0.1" diff --git a/voxygen/src/ecs/mod.rs b/voxygen/src/ecs/mod.rs index 701dbb334b..2c3e5e898a 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", |_| 1); pool.configure("FIGURE_MESHING", |n| n / 2); pool.configure("TERRAIN_MESHING", |n| n / 2); } diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 1c4dc5e8bb..1d7cd743da 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -6,7 +6,7 @@ use super::{ 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}; @@ -14,7 +14,8 @@ use common::{ comp, comp::group::Role, grid::Grid, - terrain::{Block, TerrainChunk, TerrainChunkSize}, + slowjob::SlowJobPool, + terrain::{Block, BlockKind, TerrainChunk, TerrainChunkSize}, vol::{ReadVol, RectVolSize}, }; use common_net::msg::world_msg::SiteKind; @@ -46,6 +47,9 @@ pub struct VoxelMinimap { image_id: img_ids::Rotations, last_pos: Vec3, last_ceiling: i32, + /// Maximum z of the top of the tallest loaded chunk (for ceiling pruning) + max_chunk_z: i32, + keyed_jobs: KeyedJobs, MinimapColumn>, } const VOXEL_MINIMAP_SIDELENGTH: u32 = 512; @@ -66,13 +70,22 @@ impl VoxelMinimap { composited, last_pos: Vec3::zero(), last_ceiling: 0, + max_chunk_z: 0, + keyed_jobs: KeyedJobs::new(), } } fn block_color(block: &Block) -> Option> { block .get_color() - .map(|rgb| Vec4::new(rgb.r, rgb.g, rgb.b, 192)) + .map(|rgb| Vec4::new(rgb.r, rgb.g, rgb.b, 255)) + .or_else(|| { + if matches!(block.kind(), BlockKind::Water) { + Some(Vec4::new(107, 165, 220, 255)) + } else { + None + } + }) } /// Each layer is a slice of the terrain near that z-level @@ -86,7 +99,7 @@ impl VoxelMinimap { .get(Vec3::new(v.x, v.y, dz as i32 + z + zoff)) .ok() .and_then(Self::block_color) - .unwrap_or(Vec4::zero()); + .unwrap_or_else(Vec4::zero); rgba += color.as_() * weights[dz as usize] as f32; } let rgba: Vec4 = (rgba / weights.iter().map(|x| *x as f32).sum::()).as_(); @@ -111,9 +124,10 @@ impl VoxelMinimap { .and_then(Self::block_color) { if seen_air > 0 { - rgba = Some(color.map(|j| { + /*rgba = Some(color.map(|j| { (j as u32).saturating_sub(if seen_air > 2 { 4 } else { 0 }) as u8 - })); + }));*/ + rgba = Some(color); break; } seen_solids += 1; @@ -137,40 +151,47 @@ impl VoxelMinimap { } pub fn maintain(&mut self, client: &Client, ui: &mut Ui) { + let pool = client.state().ecs().read_resource::(); let terrain = client.state().terrain(); let mut new_chunk = false; for (key, chunk) in terrain.iter() { if !self.chunk_minimaps.contains_key(&key) { - new_chunk = true; - let mut layers = Vec::new(); - const MODE_OVERHEAD: bool = true; - if MODE_OVERHEAD { - Self::composite_layer_overhead(chunk, &mut layers); - } else { - Self::composite_layer_slice(chunk, &mut layers); + let arc_chunk = Arc::clone(chunk); + if let Some((_, column)) = self.keyed_jobs.spawn(Some(&pool), key, 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() + .cloned() + .unwrap_or_else(Block::empty); + let below = arc_chunk + .get(Vec3::new(0, 0, arc_chunk.get_min_z() - 1)) + .ok() + .cloned() + .unwrap_or_else(Block::empty); + MinimapColumn { + zlo: arc_chunk.get_min_z(), + layers, + above: ( + Self::block_color(&above).unwrap_or_else(Vec4::zero), + above.is_filled(), + ), + below: ( + Self::block_color(&below).unwrap_or_else(Vec4::zero), + below.is_filled(), + ), + } + }) { + self.chunk_minimaps.insert(key, column); + new_chunk = true; + self.max_chunk_z = self.max_chunk_z.max(chunk.get_max_z()); } - let above = chunk - .get(Vec3::new(0, 0, chunk.get_max_z() + 1)) - .ok() - .cloned() - .unwrap_or_else(Block::empty); - let below = chunk - .get(Vec3::new(0, 0, chunk.get_min_z() - 1)) - .ok() - .cloned() - .unwrap_or_else(Block::empty); - self.chunk_minimaps.insert(key, MinimapColumn { - zlo: chunk.get_min_z(), - layers, - above: ( - Self::block_color(&above).unwrap_or_else(Vec4::zero), - above.is_filled(), - ), - below: ( - Self::block_color(&below).unwrap_or_else(Vec4::zero), - below.is_filled(), - ), - }); } } let player = client.entity(); @@ -208,7 +229,7 @@ impl VoxelMinimap { if above.1 { 1 } else { - layers.len() as i32 - pos.z as i32 + zlo + self.max_chunk_z - pos.z as i32 } }) }, @@ -220,7 +241,6 @@ impl VoxelMinimap { || self.last_ceiling != ceiling_offset || new_chunk { - tracing::info!("{:?} {:?}", pos, ceiling_offset); self.last_pos = cpos.with_z(pos.z as i32); self.last_ceiling = ceiling_offset; for y in 0..VOXEL_MINIMAP_SIDELENGTH { @@ -241,7 +261,7 @@ impl VoxelMinimap { layers .get( ((pos.z as i32 - zlo + ceiling_offset) as usize) - .min(layers.len() - 1), + .min(layers.len().saturating_sub(1)), ) .and_then(|grid| grid.get(cmod).map(|c| c.0.as_())) .or_else(|| { @@ -253,7 +273,7 @@ impl VoxelMinimap { }) }, ) - .unwrap_or(Vec4::zero()); + .unwrap_or_else(Vec4::zero); self.composited.put_pixel( x, VOXEL_MINIMAP_SIDELENGTH - y - 1, diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 867c9434c0..bf0b010e48 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -81,6 +81,7 @@ use common::{ }, consts::MAX_PICKUP_RANGE, outcome::Outcome, + slowjob::SlowJobPool, terrain::{SpriteKind, TerrainChunk}, trade::{ReducedInventory, TradeAction}, uid::Uid, @@ -3603,9 +3604,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/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/ui/graphic/mod.rs b/voxygen/src/ui/graphic/mod.rs index 1180b8bda4..8a9ff67f82 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(), } } @@ -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,40 @@ 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) { 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) => { + let inner = inner.clone(); + keyed_jobs + .spawn(pool, (graphic_id, dims), 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, + )), + _ => 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/mod.rs b/voxygen/src/ui/mod.rs index 64ad6ec49d..54fac9da0e 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -34,7 +34,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, @@ -48,8 +48,8 @@ use conrod_core::{ }; use core::{convert::TryInto, f32, f64, ops::Range}; use graphic::TexId; -use hashbrown::hash_map::Entry; -use std::time::Duration; +use hashbrown::{hash_map::Entry, HashMap}; +use std::{hash::Hash, time::Duration}; use tracing::{error, warn}; use vek::*; @@ -306,7 +306,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 +358,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 +812,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, @@ -1047,3 +1054,51 @@ fn default_scissor(renderer: &Renderer) -> Aabr { }, } } + +pub struct KeyedJobs { + tx: crossbeam_channel::Sender<(K, V)>, + rx: crossbeam_channel::Receiver<(K, V)>, + buf: HashMap, +} + +impl KeyedJobs { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let (tx, rx) = crossbeam_channel::unbounded(); + Self { + tx, + rx, + buf: HashMap::new(), + } + } + + pub fn spawn( + &mut self, + pool: Option<&SlowJobPool>, + k: K, + f: impl FnOnce(&K) -> V + Send + Sync + 'static, + ) -> Option<(K, V)> { + if let Some(pool) = pool { + if let Some(v) = self.buf.remove(&k) { + Some((k, v)) + } else { + while let Ok((k2, v)) = self.rx.try_recv() { + if k == k2 { + return Some((k, v)); + } else { + self.buf.insert(k2, v); + } + } + let tx = self.tx.clone(); + pool.spawn("IMAGE_PROCESSING", move || { + let v = f(&k); + let _ = tx.send((k, v)); + }); + None + } + } else { + let v = f(&k); + Some((k, v)) + } + } +} From cbe752125994e02270ab33060bc23e5b83cafa47 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 13 May 2021 20:46:17 -0400 Subject: [PATCH 07/16] Add toggle for minimap, fix scaling formula, reduce minimap VD for better performance, and add changelog entry. --- CHANGELOG.md | 1 + assets/voxygen/i18n/en/hud/map.ron | 1 + voxygen/src/hud/map.rs | 38 ++++++++++++++++++++++++++ voxygen/src/hud/minimap.rs | 7 ++--- voxygen/src/session/settings_change.rs | 4 +++ voxygen/src/settings/interface.rs | 2 ++ 6 files changed, 49 insertions(+), 4 deletions(-) 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/assets/voxygen/i18n/en/hud/map.ron b/assets/voxygen/i18n/en/hud/map.ron index 2b75841070..61f3e1ccc1 100644 --- a/assets/voxygen/i18n/en/hud/map.ron +++ b/assets/voxygen/i18n/en/hud/map.ron @@ -14,6 +14,7 @@ "hud.map.caves": "Caves", "hud.map.cave": "Cave", "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/src/hud/map.rs b/voxygen/src/hud/map.rs index fea6aa9a98..8c9c06d2fd 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,40 @@ 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 + 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 1d7cd743da..9e86e98ece 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -52,7 +52,7 @@ pub struct VoxelMinimap { keyed_jobs: KeyedJobs, MinimapColumn>, } -const VOXEL_MINIMAP_SIDELENGTH: u32 = 512; +const VOXEL_MINIMAP_SIDELENGTH: u32 = 256; impl VoxelMinimap { pub fn new(ui: &mut Ui) -> Self { let mut composited = RgbaImage::new(VOXEL_MINIMAP_SIDELENGTH, VOXEL_MINIMAP_SIDELENGTH); @@ -394,8 +394,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 show_voxel_map = true; + 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 { @@ -563,7 +562,7 @@ impl<'a> Widget for MiniMap<'a> { } else { self.voxel_minimap.image_id.source_north }; - let scaling = (VOXEL_MINIMAP_SIDELENGTH as f64 / 32.0) * max_zoom / zoom; + let scaling = 32.0 * max_zoom / zoom; let cmod: Vec2 = (player_pos.xy() % 32.0).as_(); let rect_src = position::Rect::from_xy_dim( [ 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, From 7115b1c89914268ad73e7abd4266e482eb49d269 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 14 May 2021 12:26:34 -0400 Subject: [PATCH 08/16] Performance and aesthetic improvements to the voxel minimap. Performance: - don't do anything when it's inactive - only process chunks within minimap-VD - remove chunks outside of VD - higher thread pool fraction specified Aesthetics: - different water color - wood and leaves no longer count as ceilings --- voxygen/src/ecs/mod.rs | 2 +- voxygen/src/hud/minimap.rs | 93 +++++++++++++++++++++++++++++--------- voxygen/src/hud/mod.rs | 4 +- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/voxygen/src/ecs/mod.rs b/voxygen/src/ecs/mod.rs index 2c3e5e898a..8b7b647b38 100644 --- a/voxygen/src/ecs/mod.rs +++ b/voxygen/src/ecs/mod.rs @@ -11,7 +11,7 @@ pub fn init(world: &mut World) { { let pool = world.read_resource::(); - pool.configure("IMAGE_PROCESSING", |_| 1); + 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/minimap.rs b/voxygen/src/hud/minimap.rs index 9e86e98ece..4a2fec055c 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -15,7 +15,7 @@ use common::{ comp::group::Role, grid::Grid, slowjob::SlowJobPool, - terrain::{Block, BlockKind, TerrainChunk, TerrainChunkSize}, + terrain::{Block, BlockKind, TerrainChunk, TerrainChunkSize, TerrainGrid}, vol::{ReadVol, RectVolSize}, }; use common_net::msg::world_msg::SiteKind; @@ -81,7 +81,7 @@ impl VoxelMinimap { .map(|rgb| Vec4::new(rgb.r, rgb.g, rgb.b, 255)) .or_else(|| { if matches!(block.kind(), BlockKind::Water) { - Some(Vec4::new(107, 165, 220, 255)) + Some(Vec4::new(119, 149, 197, 255)) } else { None } @@ -140,22 +140,34 @@ impl VoxelMinimap { break; } } - let is_filled = chunk - .get(Vec3::new(v.x, v.y, z)) - .ok() - .map_or(true, |b| b.is_filled()); - (rgba.unwrap_or_else(Vec4::zero), is_filled) + 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(|| Vec3::zero().with_w(255)); + (rgba, is_filled) }); layers.push(grid); } } - pub fn maintain(&mut self, client: &Client, ui: &mut Ui) { - let pool = client.state().ecs().read_resource::(); - let terrain = client.state().terrain(); - let mut new_chunk = false; + fn add_chunks_near( + &mut self, + pool: &SlowJobPool, + terrain: &TerrainGrid, + cpos: Vec2, + ) -> bool { + let mut new_chunks = false; + for (key, chunk) in terrain.iter() { - if !self.chunk_minimaps.contains_key(&key) { + let delta: Vec2 = (key - cpos).map(i32::abs).as_(); + if !self.chunk_minimaps.contains_key(&key) + && delta.x < VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.x + && delta.y < VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.y + { let arc_chunk = Arc::clone(chunk); if let Some((_, column)) = self.keyed_jobs.spawn(Some(&pool), key, move |_| { let mut layers = Vec::new(); @@ -189,23 +201,51 @@ impl VoxelMinimap { } }) { self.chunk_minimaps.insert(key, column); - new_chunk = true; + new_chunks = true; self.max_chunk_z = self.max_chunk_z.max(chunk.get_max_z()); } } } + new_chunks + } + + fn remove_unloaded_chunks(&mut self, terrain: &TerrainGrid) { + let mut removals = Vec::new(); + for key in self.chunk_minimaps.keys() { + if terrain.get_key(*key).is_none() { + removals.push(*key); + } + } + for key in removals.into_iter() { + self.chunk_minimaps.remove(&key); + } + } + + pub fn maintain(&mut self, client: &Client, ui: &mut Ui) { let player = client.entity(); if let Some(pos) = client.state().ecs().read_storage::().get(player) { let pos = pos.0; let vpos = pos.xy() - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0; - let cpos: Vec2 = vpos.map(|i| (i as i32).div_euclid(32)); + 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_unloaded_chunks(&terrain); + let ceiling_offset = { let voff = Vec2::new( VOXEL_MINIMAP_SIDELENGTH as f32, VOXEL_MINIMAP_SIDELENGTH as f32, ) / 2.0; - let coff: Vec2 = voff.map(|i| (i as i32).div_euclid(32)); - let cmod: Vec2 = vpos.map(|i| (i as i32).rem_euclid(32)); + 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( @@ -239,15 +279,19 @@ impl VoxelMinimap { if cpos.distance_squared(self.last_pos.xy()) >= 1 || self.last_pos.z != pos.z as i32 || self.last_ceiling != ceiling_offset - || new_chunk + || 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.map(|i| (i as i32).div_euclid(32)); - let cmod: Vec2 = voff.map(|i| (i as i32).rem_euclid(32)); + 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 ceiling_offset = 8; let color: Vec4 = column @@ -562,14 +606,19 @@ impl<'a> Widget for MiniMap<'a> { } else { self.voxel_minimap.image_id.source_north }; - let scaling = 32.0 * max_zoom / zoom; - let cmod: Vec2 = (player_pos.xy() % 32.0).as_(); + 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, ], - [scaling, scaling], + [ + 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) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index bf0b010e48..434f1526bb 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -960,7 +960,9 @@ impl Hud { ) -> Vec { span!(_guard, "update_layout", "Hud::update_layout"); let mut events = core::mem::take(&mut self.events); - self.voxel_minimap.maintain(&client, &mut self.ui); + 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 From 523bf2be58dde40889b1152fb7f65af0655ad245 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 14 May 2021 18:08:22 -0400 Subject: [PATCH 09/16] Fix memory leak in `KeyedJobs` threadpool helper. --- Cargo.lock | 10 ---------- voxygen/Cargo.toml | 2 +- voxygen/src/hud/minimap.rs | 1 - voxygen/src/ui/mod.rs | 28 +++++++++++++++++----------- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17d6ebc8b3..907cfb5d68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2415,15 +2415,6 @@ dependencies = [ "serde", ] -[[package]] -name = "inline_tweak" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7033e97b20277cc0d043226d1940fa7719ff08d2305d1fc7421e53066d00eb4b" -dependencies = [ - "lazy_static", -] - [[package]] name = "inotify" version = "0.7.1" @@ -5843,7 +5834,6 @@ dependencies = [ "iced_native", "iced_winit", "image", - "inline_tweak", "itertools 0.10.0", "keyboard-keynames", "lazy_static", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 1cd3a85aba..9f949936bc 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -104,7 +104,7 @@ treeculler = "0.2" tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] } num_cpus = "1.0" # vec_map = { version = "0.8.2" } -inline_tweak = "1.0.2" +# inline_tweak = "1.0.2" itertools = "0.10.0" crossbeam-channel = "0.5" diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 4a2fec055c..b095b96d71 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -293,7 +293,6 @@ impl VoxelMinimap { .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j)) .as_(); let column = self.chunk_minimaps.get(&(cpos + coff)); - //let ceiling_offset = 8; let color: Vec4 = column .and_then( |MinimapColumn { diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index 54fac9da0e..bdf750acde 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -48,7 +48,7 @@ use conrod_core::{ }; use core::{convert::TryInto, f32, f64, ops::Range}; use graphic::TexId; -use hashbrown::{hash_map::Entry, HashMap}; +use hashbrown::{hash_map::Entry, HashMap, HashSet}; use std::{hash::Hash, time::Duration}; use tracing::{error, warn}; use vek::*; @@ -1058,17 +1058,19 @@ fn default_scissor(renderer: &Renderer) -> Aabr { pub struct KeyedJobs { tx: crossbeam_channel::Sender<(K, V)>, rx: crossbeam_channel::Receiver<(K, V)>, - buf: HashMap, + completed: HashMap, + pending: HashSet, } -impl KeyedJobs { +impl KeyedJobs { #[allow(clippy::new_without_default)] pub fn new() -> Self { let (tx, rx) = crossbeam_channel::unbounded(); Self { tx, rx, - buf: HashMap::new(), + completed: HashMap::new(), + pending: HashSet::new(), } } @@ -1079,21 +1081,25 @@ impl KeyedJobs V + Send + Sync + 'static, ) -> Option<(K, V)> { if let Some(pool) = pool { - if let Some(v) = self.buf.remove(&k) { + if let Some(v) = self.completed.remove(&k) { Some((k, v)) } else { while let Ok((k2, v)) = self.rx.try_recv() { + self.pending.remove(&k2); if k == k2 { return Some((k, v)); } else { - self.buf.insert(k2, v); + self.completed.insert(k2, v); } } - let tx = self.tx.clone(); - pool.spawn("IMAGE_PROCESSING", move || { - let v = f(&k); - let _ = tx.send((k, v)); - }); + if !self.pending.contains(&k) { + self.pending.insert(k.clone()); + let tx = self.tx.clone(); + pool.spawn("IMAGE_PROCESSING", move || { + let v = f(&k); + let _ = tx.send((k, v)); + }); + } None } } else { From 9d60a7f53389cd34d521680b582223023b69330d Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 14 May 2021 19:10:17 -0400 Subject: [PATCH 10/16] Make the chunks above dungeons/caves default to black when the ceiling is below them. --- voxygen/src/hud/minimap.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index b095b96d71..6932b324fc 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -301,19 +301,23 @@ impl VoxelMinimap { above, below, }| { - layers - .get( - ((pos.z as i32 - zlo + 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 + if pos.z as i32 + ceiling_offset < *zlo { + Some(Vec3::zero().with_w(255)) + } else { + layers + .get( + ((pos.z as i32 - zlo + 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(Vec4::zero); From e509a4207b5d5252d28d5f799dc22681e6769359 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Sun, 16 May 2021 17:01:46 -0400 Subject: [PATCH 11/16] Make the minimap UI not exposed via checkbox yet (still configurable via `settings.ron`). --- voxygen/src/hud/map.rs | 70 ++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/voxygen/src/hud/map.rs b/voxygen/src/hud/map.rs index 8c9c06d2fd..f2389230ad 100644 --- a/voxygen/src/hud/map.rs +++ b/voxygen/src/hud/map.rs @@ -640,40 +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 - 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))); + // 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); } - 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| { From 6d3dcc3835636c7716ea3a79a78d4d2d2d7e2dac Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Wed, 19 May 2021 14:11:24 -0400 Subject: [PATCH 12/16] Address MR 2301 review comments. - Replace tabs with spaces in map-related i18n. - Use `retain` in `remove_unloaded_chunks`. --- assets/voxygen/i18n/en/hud/map.ron | 4 ++-- voxygen/src/hud/minimap.rs | 11 ++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/assets/voxygen/i18n/en/hud/map.ron b/assets/voxygen/i18n/en/hud/map.ron index 61f3e1ccc1..5c30f4f22c 100644 --- a/assets/voxygen/i18n/en/hud/map.ron +++ b/assets/voxygen/i18n/en/hud/map.ron @@ -13,8 +13,8 @@ "hud.map.dungeons": "Dungeons", "hud.map.caves": "Caves", "hud.map.cave": "Cave", - "hud.map.peaks": "Mountains", - "hud.map.voxel_map": "Voxel map", + "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/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 6932b324fc..42a1dac13b 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -210,15 +210,8 @@ impl VoxelMinimap { } fn remove_unloaded_chunks(&mut self, terrain: &TerrainGrid) { - let mut removals = Vec::new(); - for key in self.chunk_minimaps.keys() { - if terrain.get_key(*key).is_none() { - removals.push(*key); - } - } - for key in removals.into_iter() { - self.chunk_minimaps.remove(&key); - } + self.chunk_minimaps + .retain(|key, _| terrain.get_key(*key).is_some()); } pub fn maintain(&mut self, client: &Client, ui: &mut Ui) { From 8b21ed540c70eaf926d44e7af82bffeab6c30cd1 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Mon, 31 May 2021 12:04:18 -0400 Subject: [PATCH 13/16] Use a single HashMap with the entry API and a `KeyedJobTask` enum for `KeyedJobs`. --- voxygen/src/ui/mod.rs | 50 +++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index bdf750acde..0a4681323e 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -48,7 +48,7 @@ use conrod_core::{ }; use core::{convert::TryInto, f32, f64, ops::Range}; use graphic::TexId; -use hashbrown::{hash_map::Entry, HashMap, HashSet}; +use hashbrown::{hash_map::Entry, HashMap}; use std::{hash::Hash, time::Duration}; use tracing::{error, warn}; use vek::*; @@ -1055,11 +1055,15 @@ fn default_scissor(renderer: &Renderer) -> Aabr { } } +enum KeyedJobTask { + Pending, + Completed(V), +} + pub struct KeyedJobs { tx: crossbeam_channel::Sender<(K, V)>, rx: crossbeam_channel::Receiver<(K, V)>, - completed: HashMap, - pending: HashSet, + tasks: HashMap>, } impl KeyedJobs { @@ -1069,8 +1073,7 @@ impl Key Self { tx, rx, - completed: HashMap::new(), - pending: HashSet::new(), + tasks: HashMap::new(), } } @@ -1081,26 +1084,35 @@ impl Key f: impl FnOnce(&K) -> V + Send + Sync + 'static, ) -> Option<(K, V)> { if let Some(pool) = pool { - if let Some(v) = self.completed.remove(&k) { - Some((k, v)) - } else { - while let Ok((k2, v)) = self.rx.try_recv() { - self.pending.remove(&k2); - if k == k2 { - return Some((k, v)); - } else { - self.completed.insert(k2, v); - } + while let Ok((k2, v)) = self.rx.try_recv() { + if k == k2 { + return Some((k, v)); + } else { + self.tasks.insert(k2, KeyedJobTask::Completed(v)); } - if !self.pending.contains(&k) { - self.pending.insert(k.clone()); + } + 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) => { let tx = self.tx.clone(); pool.spawn("IMAGE_PROCESSING", move || { let v = f(&k); let _ = tx.send((k, v)); }); - } - None + e.insert(KeyedJobTask::Pending); + None + }, } } else { let v = f(&k); From 5aa98d18e37506a73ddf19a663a053e148b17fbd Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Mon, 31 May 2021 13:03:18 -0400 Subject: [PATCH 14/16] Depend on `crossbeam-utils` and `crossbeam-channel` instead of `crossbeam` in voxygen. --- Cargo.lock | 16 +--------------- voxygen/Cargo.toml | 4 ++-- voxygen/src/menu/main/client_init.rs | 2 +- voxygen/src/scene/figure/cache.rs | 2 +- voxygen/src/scene/terrain.rs | 2 +- voxygen/src/singleplayer.rs | 2 +- voxygen/src/window.rs | 2 +- 7 files changed, 8 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 907cfb5d68..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,8 +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/voxygen/Cargo.toml b/voxygen/Cargo.toml index 9f949936bc..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" @@ -106,7 +107,6 @@ num_cpus = "1.0" # vec_map = { version = "0.8.2" } # inline_tweak = "1.0.2" itertools = "0.10.0" -crossbeam-channel = "0.5" # Tracy tracing = "0.1" 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/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/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/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; From 9c4453508e2aaa04449c200e8db9d34d642db602 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Tue, 1 Jun 2021 18:39:15 -0400 Subject: [PATCH 15/16] Address many of Imbris's comments on MR 2301. --- voxygen/src/hud/minimap.rs | 334 +++++++++++++++++----------------- voxygen/src/ui/graphic/mod.rs | 39 ++-- voxygen/src/ui/mod.rs | 10 +- 3 files changed, 196 insertions(+), 187 deletions(-) diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 42a1dac13b..3ccf51afe8 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -34,11 +34,11 @@ struct MinimapColumn { /// Coordinate of lowest z-slice zlo: i32, /// Z-slices of colors and filled-ness - layers: Vec, bool)>>, + layers: Vec, bool)>>, /// Color and filledness above the highest layer - above: (Vec4, bool), + above: (Rgba, bool), /// Color and filledness below the lowest layer - below: (Vec4, bool), + below: (Rgba, bool), } pub struct VoxelMinimap { @@ -53,14 +53,14 @@ pub struct VoxelMinimap { } const VOXEL_MINIMAP_SIDELENGTH: u32 = 256; + impl VoxelMinimap { pub fn new(ui: &mut Ui) -> Self { - let mut composited = RgbaImage::new(VOXEL_MINIMAP_SIDELENGTH, VOXEL_MINIMAP_SIDELENGTH); - for x in 0..VOXEL_MINIMAP_SIDELENGTH { - for y in 0..VOXEL_MINIMAP_SIDELENGTH { - composited.put_pixel(x, y, image::Rgba([0, 0, 0, 64])); - } - } + 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( @@ -75,34 +75,30 @@ impl VoxelMinimap { } } - fn block_color(block: &Block) -> Option> { + fn block_color(block: &Block) -> Option> { block .get_color() - .map(|rgb| Vec4::new(rgb.r, rgb.g, rgb.b, 255)) + .map(|rgb| Rgba::new(rgb.r, rgb.g, rgb.b, 255)) .or_else(|| { - if matches!(block.kind(), BlockKind::Water) { - Some(Vec4::new(119, 149, 197, 255)) - } else { - None - } + 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)>>) { + 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 = Vec4::::zero(); + 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(Vec4::zero); + .unwrap_or_else(Rgba::zero); rgba += color.as_() * weights[dz as usize] as f32; } - let rgba: Vec4 = (rgba / weights.iter().map(|x| *x as f32).sum::()).as_(); + let rgba: Rgba = (rgba / weights.iter().map(|x| *x as f32).sum::()).as_(); (rgba, true) }); layers.push(grid); @@ -110,7 +106,7 @@ impl VoxelMinimap { } /// Each layer is the overhead as if its z-level were the ceiling - fn composite_layer_overhead(chunk: &TerrainChunk, layers: &mut Vec, bool)>>) { + 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; @@ -124,9 +120,6 @@ impl VoxelMinimap { .and_then(Self::block_color) { if seen_air > 0 { - /*rgba = Some(color.map(|j| { - (j as u32).saturating_sub(if seen_air > 2 { 4 } else { 0 }) as u8 - }));*/ rgba = Some(color); break; } @@ -147,7 +140,7 @@ impl VoxelMinimap { let is_filled = block.map_or(true, |b| { b.is_filled() && !matches!(b.kind(), BlockKind::Leaves | BlockKind::Wood) }); - let rgba = rgba.unwrap_or_else(|| Vec3::zero().with_w(255)); + let rgba = rgba.unwrap_or_else(|| Rgba::new(0, 0, 0, 255)); (rgba, is_filled) }); layers.push(grid); @@ -164,40 +157,42 @@ impl VoxelMinimap { for (key, chunk) in terrain.iter() { let delta: Vec2 = (key - cpos).map(i32::abs).as_(); - if !self.chunk_minimaps.contains_key(&key) - && delta.x < VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.x + 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) { - let arc_chunk = Arc::clone(chunk); - if let Some((_, column)) = self.keyed_jobs.spawn(Some(&pool), key, 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() - .cloned() - .unwrap_or_else(Block::empty); - let below = arc_chunk - .get(Vec3::new(0, 0, arc_chunk.get_min_z() - 1)) - .ok() - .cloned() - .unwrap_or_else(Block::empty); - MinimapColumn { - zlo: arc_chunk.get_min_z(), - layers, - above: ( - Self::block_color(&above).unwrap_or_else(Vec4::zero), - above.is_filled(), - ), - below: ( - Self::block_color(&below).unwrap_or_else(Vec4::zero), - below.is_filled(), - ), + 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); @@ -216,120 +211,127 @@ impl VoxelMinimap { pub fn maintain(&mut self, client: &Client, ui: &mut Ui) { let player = client.entity(); - if let Some(pos) = client.state().ecs().read_storage::().get(player) { - let pos = pos.0; - let vpos = pos.xy() - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0; - let cpos: Vec2 = vpos + 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_unloaded_chunks(&terrain); + + // 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 pool = client.state().ecs().read_resource::(); - let terrain = client.state().terrain(); - let new_chunks = self.add_chunks_near(&pool, &terrain, cpos); - self.remove_unloaded_chunks(&terrain); - - 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) - .filter_map(|dz| { - layers.get((pos.z as i32 - zlo + dz) as usize).and_then( - |grid| { - if grid.get(cmod).map_or(false, |(_, b)| *b) { - Some(dz) - } else { - None - } - }, + 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 above.1 { + 1 + } else { + self.max_chunk_z - pos.z as i32 + } + }) + }, + ) + .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 + 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 + ceiling_offset) as usize) + .min(layers.len().saturating_sub(1)), ) - }) - .next() - .unwrap_or_else(|| { - if above.1 { - 1 - } else { - self.max_chunk_z - pos.z as i32 - } - }) - }, - ) - .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: Vec4 = column - .and_then( - |MinimapColumn { - zlo, - layers, - above, - below, - }| { - if pos.z as i32 + ceiling_offset < *zlo { - Some(Vec3::zero().with_w(255)) - } else { - layers - .get( - ((pos.z as i32 - zlo + 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(Vec4::zero); - self.composited.put_pixel( - x, - VOXEL_MINIMAP_SIDELENGTH - y - 1, - image::Rgba([color.x, color.y, color.z, color.w]), - ); - } + .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])), - ), - ); } + + 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])), + ), + ); } } } diff --git a/voxygen/src/ui/graphic/mod.rs b/voxygen/src/ui/graphic/mod.rs index 8a9ff67f82..4c7480efbb 100644 --- a/voxygen/src/ui/graphic/mod.rs +++ b/voxygen/src/ui/graphic/mod.rs @@ -402,27 +402,30 @@ fn draw_graphic( pool: Option<&SlowJobPool>, ) -> Option<(RgbaImage, Option>)> { match graphic_map.get(&graphic_id) { + // Short-circuit spawning a threadpool for blank graphics Some(Graphic::Blank) => None, Some(inner) => { - let inner = inner.clone(); keyed_jobs - .spawn(pool, (graphic_id, dims), 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, - )), - _ => None, + .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) diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index 0a4681323e..256173b248 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -1077,11 +1077,14 @@ impl Key } } - pub fn spawn( + /// 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(&K) -> V + Send + Sync + 'static, + f: impl FnOnce() -> F, ) -> Option<(K, V)> { if let Some(pool) = pool { while let Ok((k2, v)) = self.rx.try_recv() { @@ -1106,6 +1109,7 @@ impl Key }, Entry::Vacant(e) => { let tx = self.tx.clone(); + let f = f(); pool.spawn("IMAGE_PROCESSING", move || { let v = f(&k); let _ = tx.send((k, v)); @@ -1115,7 +1119,7 @@ impl Key }, } } else { - let v = f(&k); + let v = f()(&k); Some((k, v)) } } From 42dc86788ee3b0e161f3b23e890659f00104f4ca Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Tue, 1 Jun 2021 23:59:15 -0400 Subject: [PATCH 16/16] Address further MR 2301 comments. --- voxygen/src/hud/minimap.rs | 33 +++++++---- voxygen/src/ui/graphic/mod.rs | 4 +- voxygen/src/ui/keyed_jobs.rs | 102 ++++++++++++++++++++++++++++++++++ voxygen/src/ui/mod.rs | 76 ++----------------------- 4 files changed, 129 insertions(+), 86 deletions(-) create mode 100644 voxygen/src/ui/keyed_jobs.rs diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 3ccf51afe8..7eb760a8aa 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -47,8 +47,6 @@ pub struct VoxelMinimap { image_id: img_ids::Rotations, last_pos: Vec3, last_ceiling: i32, - /// Maximum z of the top of the tallest loaded chunk (for ceiling pruning) - max_chunk_z: i32, keyed_jobs: KeyedJobs, MinimapColumn>, } @@ -70,8 +68,7 @@ impl VoxelMinimap { composited, last_pos: Vec3::zero(), last_ceiling: 0, - max_chunk_z: 0, - keyed_jobs: KeyedJobs::new(), + keyed_jobs: KeyedJobs::new("IMAGE_PROCESSING"), } } @@ -197,16 +194,19 @@ impl VoxelMinimap { }) { self.chunk_minimaps.insert(key, column); new_chunks = true; - self.max_chunk_z = self.max_chunk_z.max(chunk.get_max_z()); } } } new_chunks } - fn remove_unloaded_chunks(&mut self, terrain: &TerrainGrid) { - self.chunk_minimaps - .retain(|key, _| terrain.get_key(*key).is_some()); + 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) { @@ -224,7 +224,7 @@ impl VoxelMinimap { let pool = client.state().ecs().read_resource::(); let terrain = client.state().terrain(); let new_chunks = self.add_chunks_near(&pool, &terrain, cpos); - self.remove_unloaded_chunks(&terrain); + 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 @@ -256,10 +256,18 @@ impl VoxelMinimap { .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 { - self.max_chunk_z - pos.z as i32 + // if the ceiling is a non-solid sky, use the largest value + // (subsequent arithmetic on ceiling_offset must be saturating) + i32::MAX } }) }, @@ -291,7 +299,7 @@ impl VoxelMinimap { above, below, } = column; - if pos.z as i32 + ceiling_offset < *zlo { + 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. @@ -303,7 +311,8 @@ impl VoxelMinimap { // differently-tall trees are handled properly) layers .get( - ((pos.z as i32 - zlo + ceiling_offset) as usize) + (((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_())) diff --git a/voxygen/src/ui/graphic/mod.rs b/voxygen/src/ui/graphic/mod.rs index 4c7480efbb..241e14dbbc 100644 --- a/voxygen/src/ui/graphic/mod.rs +++ b/voxygen/src/ui/graphic/mod.rs @@ -159,7 +159,7 @@ impl GraphicCache { atlases: vec![(atlas, 0)], textures: vec![texture], cache_map: HashMap::default(), - keyed_jobs: KeyedJobs::new(), + keyed_jobs: KeyedJobs::new("IMAGE_PROCESSING"), } } @@ -402,7 +402,7 @@ fn draw_graphic( pool: Option<&SlowJobPool>, ) -> Option<(RgbaImage, Option>)> { match graphic_map.get(&graphic_id) { - // Short-circuit spawning a threadpool for blank graphics + // Short-circuit spawning a job on the threadpool for blank graphics Some(Graphic::Blank) => None, Some(inner) => { keyed_jobs 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 256173b248..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, @@ -48,8 +50,8 @@ use conrod_core::{ }; use core::{convert::TryInto, f32, f64, ops::Range}; use graphic::TexId; -use hashbrown::{hash_map::Entry, HashMap}; -use std::{hash::Hash, time::Duration}; +use hashbrown::hash_map::Entry; +use std::time::Duration; use tracing::{error, warn}; use vek::*; @@ -1054,73 +1056,3 @@ fn default_scissor(renderer: &Renderer) -> Aabr { }, } } - -enum KeyedJobTask { - Pending, - Completed(V), -} - -pub struct KeyedJobs { - tx: crossbeam_channel::Sender<(K, V)>, - rx: crossbeam_channel::Receiver<(K, V)>, - tasks: HashMap>, -} - -impl KeyedJobs { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - let (tx, rx) = crossbeam_channel::unbounded(); - Self { - tx, - rx, - tasks: HashMap::new(), - } - } - - /// 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(v)); - } - } - 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) => { - let tx = self.tx.clone(); - let f = f(); - pool.spawn("IMAGE_PROCESSING", move || { - let v = f(&k); - let _ = tx.send((k, v)); - }); - e.insert(KeyedJobTask::Pending); - None - }, - } - } else { - let v = f()(&k); - Some((k, v)) - } - } -}