Merge branch 'aweinstock/dungeonmap' into 'master'

Add voxel data to the minimap (for visualizing trees and houses and dungeons)

See merge request veloren/veloren!2301
This commit is contained in:
Imbris 2021-06-02 05:06:24 +00:00
commit 2af6cb07f8
22 changed files with 616 additions and 54 deletions

View File

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

17
Cargo.lock generated
View File

@ -1032,20 +1032,6 @@ dependencies = [
"itertools 0.9.0",
]
[[package]]
name = "crossbeam"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd01a6eb3daaafa260f6fc94c3a6c36390abc2080e38e3e34ced87393fb77d80"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-channel",
"crossbeam-deque 0.8.0",
"crossbeam-epoch 0.9.3",
"crossbeam-queue",
"crossbeam-utils 0.8.3",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.0"
@ -5815,7 +5801,8 @@ dependencies = [
"coreaudio-sys",
"cpal",
"criterion",
"crossbeam",
"crossbeam-channel",
"crossbeam-utils 0.8.3",
"directories-next",
"dispatch 0.1.4",
"dot_vox",

View File

@ -13,7 +13,8 @@
"hud.map.dungeons": "Dungeons",
"hud.map.caves": "Caves",
"hud.map.cave": "Cave",
"hud.map.peaks": "Mountains",
"hud.map.peaks": "Mountains",
"hud.map.voxel_map": "Voxel map",
"hud.map.trees": "Giant Trees",
"hud.map.tree": "Giant Tree",
"hud.map.town": "Town",

View File

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

View File

@ -11,6 +11,7 @@ pub fn init(world: &mut World) {
{
let pool = world.read_resource::<SlowJobPool>();
pool.configure("IMAGE_PROCESSING", |n| n / 2);
pool.configure("FIGURE_MESHING", |n| n / 2);
pool.configure("TERRAIN_MESHING", |n| n / 2);
}

View File

@ -65,6 +65,9 @@ widget_ids! {
show_peaks_img,
show_peaks_box,
show_peaks_text,
show_voxel_map_img,
show_voxel_map_box,
show_voxel_map_text,
show_difficulty_img,
show_difficulty_box,
show_difficulty_text,
@ -200,6 +203,7 @@ impl<'a> Widget for Map<'a> {
let show_caves = self.global_state.settings.interface.map_show_caves;
let show_trees = self.global_state.settings.interface.map_show_trees;
let show_peaks = self.global_state.settings.interface.map_show_peaks;
let show_voxel_map = self.global_state.settings.interface.map_show_voxel_map;
let show_topo_map = self.global_state.settings.interface.map_show_topo_map;
let mut events = Vec::new();
let i18n = &self.localized_strings;
@ -636,6 +640,44 @@ impl<'a> Widget for Map<'a> {
.graphics_for(state.ids.show_peaks_box)
.color(TEXT_COLOR)
.set(state.ids.show_peaks_text, ui);
// Voxel map (TODO: enable this once Pfau approves the final UI, and once
// there's a non-placeholder graphic for the checkbox)
const EXPOSE_VOXEL_MAP_TOGGLE_IN_UI: bool = false;
if EXPOSE_VOXEL_MAP_TOGGLE_IN_UI {
Image::new(self.imgs.mmap_poi_peak)
.down_from(state.ids.show_peaks_img, 10.0)
.w_h(20.0, 20.0)
.set(state.ids.show_voxel_map_img, ui);
if Button::image(if show_voxel_map {
self.imgs.checkbox_checked
} else {
self.imgs.checkbox
})
.w_h(18.0, 18.0)
.hover_image(if show_voxel_map {
self.imgs.checkbox_checked_mo
} else {
self.imgs.checkbox_mo
})
.press_image(if show_voxel_map {
self.imgs.checkbox_checked
} else {
self.imgs.checkbox_press
})
.right_from(state.ids.show_voxel_map_img, 10.0)
.set(state.ids.show_voxel_map_box, ui)
.was_clicked()
{
events.push(Event::SettingsChange(MapShowVoxelMap(!show_voxel_map)));
}
Text::new(i18n.get("hud.map.voxel_map"))
.right_from(state.ids.show_voxel_map_box, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(state.ids.show_voxel_map_box)
.color(TEXT_COLOR)
.set(state.ids.show_voxel_map_text, ui);
}
// Map icons
if state.ids.mmap_poi_icons.len() < self.client.pois().len() {
state.update(|state| {

View File

@ -4,22 +4,347 @@ use super::{
TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
};
use crate::{
hud::{Graphic, Ui},
session::settings_change::{Interface as InterfaceChange, Interface::*},
ui::{fonts::Fonts, img_ids},
ui::{fonts::Fonts, img_ids, KeyedJobs},
GlobalState,
};
use client::{self, Client};
use common::{comp, comp::group::Role, terrain::TerrainChunkSize, vol::RectVolSize};
use common::{
comp,
comp::group::Role,
grid::Grid,
slowjob::SlowJobPool,
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkSize, TerrainGrid},
vol::{ReadVol, RectVolSize},
};
use common_net::msg::world_msg::SiteKind;
use conrod_core::{
color, position,
widget::{self, Button, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};
use hashbrown::HashMap;
use image::{DynamicImage, RgbaImage};
use specs::{saveload::MarkerAllocator, WorldExt};
use std::sync::Arc;
use vek::*;
struct MinimapColumn {
/// Coordinate of lowest z-slice
zlo: i32,
/// Z-slices of colors and filled-ness
layers: Vec<Grid<(Rgba<u8>, bool)>>,
/// Color and filledness above the highest layer
above: (Rgba<u8>, bool),
/// Color and filledness below the lowest layer
below: (Rgba<u8>, bool),
}
pub struct VoxelMinimap {
chunk_minimaps: HashMap<Vec2<i32>, MinimapColumn>,
composited: RgbaImage,
image_id: img_ids::Rotations,
last_pos: Vec3<i32>,
last_ceiling: i32,
keyed_jobs: KeyedJobs<Vec2<i32>, MinimapColumn>,
}
const VOXEL_MINIMAP_SIDELENGTH: u32 = 256;
impl VoxelMinimap {
pub fn new(ui: &mut Ui) -> Self {
let composited = RgbaImage::from_pixel(
VOXEL_MINIMAP_SIDELENGTH,
VOXEL_MINIMAP_SIDELENGTH,
image::Rgba([0, 0, 0, 64]),
);
Self {
chunk_minimaps: HashMap::new(),
image_id: ui.add_graphic_with_rotations(Graphic::Image(
Arc::new(DynamicImage::ImageRgba8(composited.clone())),
Some(Rgba::from([0.0, 0.0, 0.0, 0.0])),
)),
composited,
last_pos: Vec3::zero(),
last_ceiling: 0,
keyed_jobs: KeyedJobs::new("IMAGE_PROCESSING"),
}
}
fn block_color(block: &Block) -> Option<Rgba<u8>> {
block
.get_color()
.map(|rgb| Rgba::new(rgb.r, rgb.g, rgb.b, 255))
.or_else(|| {
matches!(block.kind(), BlockKind::Water).then(|| Rgba::new(119, 149, 197, 255))
})
}
/// Each layer is a slice of the terrain near that z-level
fn composite_layer_slice(chunk: &TerrainChunk, layers: &mut Vec<Grid<(Rgba<u8>, bool)>>) {
for z in chunk.get_min_z()..chunk.get_max_z() {
let grid = Grid::populate_from(Vec2::new(32, 32), |v| {
let mut rgba = Rgba::<f32>::zero();
let (weights, zoff) = (&[1, 2, 4, 1, 1, 1][..], -2);
for dz in 0..weights.len() {
let color = chunk
.get(Vec3::new(v.x, v.y, dz as i32 + z + zoff))
.ok()
.and_then(Self::block_color)
.unwrap_or_else(Rgba::zero);
rgba += color.as_() * weights[dz as usize] as f32;
}
let rgba: Rgba<u8> = (rgba / weights.iter().map(|x| *x as f32).sum::<f32>()).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<Grid<(Rgba<u8>, bool)>>) {
for z in chunk.get_min_z()..chunk.get_max_z() {
let grid = Grid::populate_from(Vec2::new(32, 32), |v| {
let mut rgba = None;
let mut seen_solids: u32 = 0;
let mut seen_air: u32 = 0;
for dz in chunk.get_min_z()..=z {
if let Some(color) = chunk
.get(Vec3::new(v.x, v.y, z - dz + chunk.get_min_z()))
.ok()
.and_then(Self::block_color)
{
if seen_air > 0 {
rgba = Some(color);
break;
}
seen_solids += 1;
} else {
seen_air += 1;
}
// Don't penetrate too far into ground, only penetrate through shallow
// ceilings
if seen_solids > 12 {
break;
}
}
let block = chunk.get(Vec3::new(v.x, v.y, z)).ok();
// Treat Leaves and Wood as translucent for the purposes of ceiling checks,
// since otherwise trees would cause ceiling removal to trigger
// when running under a branch.
let is_filled = block.map_or(true, |b| {
b.is_filled() && !matches!(b.kind(), BlockKind::Leaves | BlockKind::Wood)
});
let rgba = rgba.unwrap_or_else(|| Rgba::new(0, 0, 0, 255));
(rgba, is_filled)
});
layers.push(grid);
}
}
fn add_chunks_near(
&mut self,
pool: &SlowJobPool,
terrain: &TerrainGrid,
cpos: Vec2<i32>,
) -> bool {
let mut new_chunks = false;
for (key, chunk) in terrain.iter() {
let delta: Vec2<u32> = (key - cpos).map(i32::abs).as_();
if delta.x < VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.x
&& delta.y < VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.y
&& !self.chunk_minimaps.contains_key(&key)
{
if let Some((_, column)) = self.keyed_jobs.spawn(Some(&pool), key, || {
let arc_chunk = Arc::clone(chunk);
move |_| {
let mut layers = Vec::new();
const MODE_OVERHEAD: bool = true;
if MODE_OVERHEAD {
Self::composite_layer_overhead(&arc_chunk, &mut layers);
} else {
Self::composite_layer_slice(&arc_chunk, &mut layers);
}
let above = arc_chunk
.get(Vec3::new(0, 0, arc_chunk.get_max_z() + 1))
.ok()
.copied()
.unwrap_or_else(Block::empty);
let below = arc_chunk
.get(Vec3::new(0, 0, arc_chunk.get_min_z() - 1))
.ok()
.copied()
.unwrap_or_else(Block::empty);
MinimapColumn {
zlo: arc_chunk.get_min_z(),
layers,
above: (
Self::block_color(&above).unwrap_or_else(Rgba::zero),
above.is_filled(),
),
below: (
Self::block_color(&below).unwrap_or_else(Rgba::zero),
below.is_filled(),
),
}
}
}) {
self.chunk_minimaps.insert(key, column);
new_chunks = true;
}
}
}
new_chunks
}
fn remove_chunks_far(&mut self, terrain: &TerrainGrid, cpos: Vec2<i32>) {
self.chunk_minimaps.retain(|key, _| {
let delta: Vec2<u32> = (key - cpos).map(i32::abs).as_();
delta.x < 1 + VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.x
&& delta.y < 1 + VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.y
&& terrain.get_key(*key).is_some()
});
}
pub fn maintain(&mut self, client: &Client, ui: &mut Ui) {
let player = client.entity();
let pos = if let Some(pos) = client.state().ecs().read_storage::<comp::Pos>().get(player) {
pos.0
} else {
return;
};
let vpos = pos.xy() - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0;
let cpos: Vec2<i32> = vpos
.map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j))
.as_();
let pool = client.state().ecs().read_resource::<SlowJobPool>();
let terrain = client.state().terrain();
let new_chunks = self.add_chunks_near(&pool, &terrain, cpos);
self.remove_chunks_far(&terrain, cpos);
// ceiling_offset is the distance from the player to a block heuristically
// detected as the ceiling height (a non-tree solid block above them, or
// the sky if no such block exists). This is used for determining which
// z-slice of the minimap to show, such that house roofs and caves and
// dungeons are all handled uniformly.
let ceiling_offset = {
let voff = Vec2::new(
VOXEL_MINIMAP_SIDELENGTH as f32,
VOXEL_MINIMAP_SIDELENGTH as f32,
) / 2.0;
let coff: Vec2<i32> = voff
.map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j))
.as_();
let cmod: Vec2<i32> = vpos
.map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j))
.as_();
let column = self.chunk_minimaps.get(&(cpos + coff));
column
.map(
|MinimapColumn {
zlo, layers, above, ..
}| {
(0..layers.len() as i32)
.find(|dz| {
layers
.get((pos.z as i32 - zlo + dz) as usize)
.and_then(|grid| grid.get(cmod))
.map_or(false, |(_, b)| *b)
})
.unwrap_or_else(|| {
// if the `find` returned None, there's no solid blocks above the
// player within the chunk
if above.1 {
// if the `above` block is solid, the chunk has an infinite
// solid ceiling, and so we render from 1 block above the
// player (which is where the player's head is if they're 2
// blocks tall)
1
} else {
// if the ceiling is a non-solid sky, use the largest value
// (subsequent arithmetic on ceiling_offset must be saturating)
i32::MAX
}
})
},
)
.unwrap_or(0)
};
if cpos.distance_squared(self.last_pos.xy()) >= 1
|| self.last_pos.z != pos.z as i32
|| self.last_ceiling != ceiling_offset
|| new_chunks
{
self.last_pos = cpos.with_z(pos.z as i32);
self.last_ceiling = ceiling_offset;
for y in 0..VOXEL_MINIMAP_SIDELENGTH {
for x in 0..VOXEL_MINIMAP_SIDELENGTH {
let voff = Vec2::new(x as f32, y as f32);
let coff: Vec2<i32> = voff
.map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j))
.as_();
let cmod: Vec2<i32> = 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<u8> = column
.and_then(|column| {
let MinimapColumn {
zlo,
layers,
above,
below,
} = column;
if (pos.z as i32).saturating_add(ceiling_offset) < *zlo {
// If the ceiling is below the bottom of a chunk, color it black,
// so that the middles of caves/dungeons don't show the forests
// around them.
Some(Rgba::new(0, 0, 0, 255))
} else {
// Otherwise, take the pixel from the precomputed z-level view at
// the ceiling's height (using the top slice of the chunk if the
// ceiling is above the chunk, (e.g. so that forests with
// differently-tall trees are handled properly)
layers
.get(
(((pos.z as i32 - zlo).saturating_add(ceiling_offset))
as usize)
.min(layers.len().saturating_sub(1)),
)
.and_then(|grid| grid.get(cmod).map(|c| c.0.as_()))
.or_else(|| {
Some(if pos.z as i32 > *zlo {
above.0
} else {
below.0
})
})
}
})
.unwrap_or_else(Rgba::zero);
self.composited.put_pixel(
x,
VOXEL_MINIMAP_SIDELENGTH - y - 1,
image::Rgba([color.r, color.g, color.b, color.a]),
);
}
}
ui.replace_graphic(
self.image_id.none,
Graphic::Image(
Arc::new(DynamicImage::ImageRgba8(self.composited.clone())),
Some(Rgba::from([0.0, 0.0, 0.0, 0.0])),
),
);
}
}
}
widget_ids! {
struct Ids {
mmap_frame,
@ -40,6 +365,7 @@ widget_ids! {
mmap_site_icons[],
member_indicators[],
location_marker,
voxel_minimap,
}
}
@ -56,6 +382,7 @@ pub struct MiniMap<'a> {
ori: Vec3<f32>,
global_state: &'a GlobalState,
location_marker: Option<Vec2<f32>>,
voxel_minimap: &'a VoxelMinimap,
}
impl<'a> MiniMap<'a> {
@ -69,6 +396,7 @@ impl<'a> MiniMap<'a> {
ori: Vec3<f32>,
global_state: &'a GlobalState,
location_marker: Option<Vec2<f32>>,
voxel_minimap: &'a VoxelMinimap,
) -> Self {
Self {
show,
@ -81,6 +409,7 @@ impl<'a> MiniMap<'a> {
ori,
global_state,
location_marker,
voxel_minimap,
}
}
}
@ -116,6 +445,7 @@ impl<'a> Widget for MiniMap<'a> {
let show_minimap = self.global_state.settings.interface.minimap_show;
let is_facing_north = self.global_state.settings.interface.minimap_face_north;
let show_topo_map = self.global_state.settings.interface.map_show_topo_map;
let show_voxel_map = self.global_state.settings.interface.map_show_voxel_map;
let orientation = if is_facing_north {
Vec3::new(0.0, 1.0, 0.0)
} else {
@ -277,6 +607,34 @@ impl<'a> Widget for MiniMap<'a> {
.set(state.ids.map_layers[index], ui);
}
}
if show_voxel_map {
let voxelmap_rotation = if is_facing_north {
self.voxel_minimap.image_id.none
} else {
self.voxel_minimap.image_id.source_north
};
let cmod: Vec2<f64> = player_pos
.xy()
.map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j))
.as_();
let rect_src = position::Rect::from_xy_dim(
[
cmod.x + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
-cmod.y + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
],
[
TerrainChunkSize::RECT_SIZE.x as f64 * max_zoom / zoom,
TerrainChunkSize::RECT_SIZE.y as f64 * max_zoom / zoom,
],
);
Image::new(voxelmap_rotation)
.middle_of(state.ids.mmap_frame_bg)
.w_h(map_size.x, map_size.y)
.parent(state.ids.mmap_frame_bg)
.source_rectangle(rect_src)
.graphics_for(state.ids.map_layers[0])
.set(state.ids.voxel_minimap, ui);
}
// Map icons
if state.ids.mmap_site_icons.len() < self.client.sites().len() {

View File

@ -42,7 +42,7 @@ use img_ids::Imgs;
use item_imgs::ItemImgs;
use loot_scroller::LootScroller;
use map::Map;
use minimap::MiniMap;
use minimap::{MiniMap, VoxelMinimap};
use popup::Popup;
use prompt_dialog::PromptDialog;
use serde::{Deserialize, Serialize};
@ -81,6 +81,7 @@ use common::{
},
consts::MAX_PICKUP_RANGE,
outcome::Outcome,
slowjob::SlowJobPool,
terrain::{SpriteKind, TerrainChunk},
trade::{ReducedInventory, TradeAction},
uid::Uid,
@ -810,6 +811,7 @@ pub struct Hud {
events: Vec<Event>,
crosshair_opacity: f32,
floaters: Floaters,
voxel_minimap: VoxelMinimap,
}
impl Hud {
@ -866,6 +868,7 @@ impl Hud {
);
Self {
voxel_minimap: VoxelMinimap::new(&mut ui),
ui,
imgs,
world_map,
@ -957,6 +960,9 @@ impl Hud {
) -> Vec<Event> {
span!(_guard, "update_layout", "Hud::update_layout");
let mut events = core::mem::take(&mut self.events);
if global_state.settings.interface.map_show_voxel_map {
self.voxel_minimap.maintain(&client, &mut self.ui);
}
let (ref mut ui_widgets, ref mut item_tooltip_manager, ref mut tooltip_manager) =
&mut self.ui.set_widgets();
// self.ui.set_item_widgets(); pulse time for pulsating elements
@ -2364,6 +2370,7 @@ impl Hud {
camera.get_orientation(),
&global_state,
self.show.location_marker,
&self.voxel_minimap,
)
.set(self.ids.minimap, ui_widgets)
{
@ -3599,9 +3606,14 @@ impl Hud {
// Check if item images need to be reloaded
self.item_imgs.reload_if_changed(&mut self.ui);
// TODO: using a thread pool in the obvious way for speeding up map zoom results
// in flickering artifacts, figure out a better way to make use of the
// thread pool
let _pool = client.state().ecs().read_resource::<SlowJobPool>();
self.ui.maintain(
&mut global_state.window.renderer_mut(),
None,
//Some(&pool),
Some(proj_mat * view_mat * Mat4::translation_3d(-focus_off)),
);

View File

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

View File

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

View File

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

View File

@ -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::*;

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Texture>,
// Stores the location of graphics rendered at a particular resolution and cached on the cpu
cache_map: HashMap<Parameters, CachedDetails>,
#[allow(clippy::type_complexity)]
keyed_jobs: KeyedJobs<(Id, Vec2<u16>), Option<(RgbaImage, Option<Rgba<f32>>)>>,
}
impl GraphicCache {
pub fn new(renderer: &mut Renderer) -> Self {
@ -153,6 +159,7 @@ impl GraphicCache {
atlases: vec![(atlas, 0)],
textures: vec![texture],
cache_map: HashMap::default(),
keyed_jobs: KeyedJobs::new("IMAGE_PROCESSING"),
}
}
@ -222,6 +229,7 @@ impl GraphicCache {
pub fn cache_res(
&mut self,
renderer: &mut Renderer,
pool: Option<&SlowJobPool>,
graphic_id: Id,
dims: Vec2<u16>,
source: Aabr<f64>,
@ -277,7 +285,8 @@ impl GraphicCache {
// graphic
if !valid {
// Create image
let (image, border) = draw_graphic(graphic_map, graphic_id, dims)?;
let (image, border) =
draw_graphic(graphic_map, graphic_id, dims, &mut self.keyed_jobs, pool)?;
// If the cache location is invalid, we know the underlying texture is mutable,
// so we should be able to replace the graphic. However, we still want to make
// sure that we are not reusing textures for images that specify a border
@ -292,8 +301,10 @@ impl GraphicCache {
Entry::Vacant(details) => details,
};
// Construct image
let (image, border_color) = draw_graphic(graphic_map, graphic_id, dims)?;
// Construct image in a threadpool
let (image, border_color) =
draw_graphic(graphic_map, graphic_id, dims, &mut self.keyed_jobs, pool)?;
// Upload
let atlas_size = atlas_size(renderer);
@ -382,23 +393,43 @@ impl GraphicCache {
}
// Draw a graphic at the specified dimensions
#[allow(clippy::type_complexity)]
fn draw_graphic(
graphic_map: &GraphicMap,
graphic_id: Id,
dims: Vec2<u16>,
keyed_jobs: &mut KeyedJobs<(Id, Vec2<u16>), Option<(RgbaImage, Option<Rgba<f32>>)>>,
pool: Option<&SlowJobPool>,
) -> Option<(RgbaImage, Option<Rgba<f32>>)> {
match graphic_map.get(&graphic_id) {
// Short-circuit spawning a job on the threadpool for blank graphics
Some(Graphic::Blank) => None,
// Render image at requested resolution
// TODO: Use source aabr.
Some(&Graphic::Image(ref image, border_color)) => Some((
resize_pixel_art(&image.to_rgba8(), u32::from(dims.x), u32::from(dims.y)),
border_color,
)),
Some(Graphic::Voxel(ref segment, trans, sample_strat)) => Some((
renderer::draw_vox(&segment, dims, trans.clone(), *sample_strat),
None,
)),
Some(inner) => {
keyed_jobs
.spawn(pool, (graphic_id, dims), || {
let inner = inner.clone();
move |_| {
match inner {
// Render image at requested resolution
// TODO: Use source aabr.
Graphic::Image(ref image, border_color) => Some((
resize_pixel_art(
&image.to_rgba8(),
u32::from(dims.x),
u32::from(dims.y),
),
border_color,
)),
Graphic::Voxel(ref segment, trans, sample_strat) => Some((
renderer::draw_vox(&segment, dims, trans, sample_strat),
None,
)),
Graphic::Blank => None,
}
}
})
.and_then(|(_, v)| v)
},
None => {
warn!(
?graphic_id,

View File

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

View File

@ -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<u32>,
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 => {},
}

View File

@ -0,0 +1,102 @@
use common::slowjob::{SlowJob, SlowJobPool};
use hashbrown::{hash_map::Entry, HashMap};
use std::{
hash::Hash,
time::{Duration, Instant},
};
enum KeyedJobTask<V> {
Pending(Instant, Option<SlowJob>),
Completed(Instant, V),
}
pub struct KeyedJobs<K, V> {
tx: crossbeam_channel::Sender<(K, V)>,
rx: crossbeam_channel::Receiver<(K, V)>,
tasks: HashMap<K, KeyedJobTask<V>>,
name: &'static str,
last_gc: Instant,
}
const KEYEDJOBS_GC_INTERVAL: Duration = Duration::from_secs(1);
impl<K: Hash + Eq + Send + Sync + 'static + Clone, V: Send + Sync + 'static> KeyedJobs<K, V> {
#[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<F: FnOnce(&K) -> 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))
}
}
}

View File

@ -8,9 +8,11 @@ pub mod img_ids;
#[macro_use]
pub mod fonts;
pub mod ice;
pub mod keyed_jobs;
pub use event::Event;
pub use graphic::{Graphic, Id as GraphicId, Rotation, SampleStrat, Transform};
pub use keyed_jobs::KeyedJobs;
pub use scale::{Scale, ScaleMode};
pub use widgets::{
image_frame::ImageFrame,
@ -34,7 +36,7 @@ use crate::{
#[rustfmt::skip]
use ::image::GenericImageView;
use cache::Cache;
use common::util::srgba_to_linear;
use common::{slowjob::SlowJobPool, util::srgba_to_linear};
use common_base::span;
use conrod_core::{
event::Input,
@ -306,7 +308,12 @@ impl Ui {
pub fn widget_input(&self, id: widget::Id) -> Widget { self.ui.widget_input(id) }
#[allow(clippy::float_cmp)] // TODO: Pending review in #587
pub fn maintain(&mut self, renderer: &mut Renderer, view_projection_mat: Option<Mat4<f32>>) {
pub fn maintain(
&mut self,
renderer: &mut Renderer,
pool: Option<&SlowJobPool>,
view_projection_mat: Option<Mat4<f32>>,
) {
span!(_guard, "maintain", "Ui::maintain");
// Maintain tooltip manager
self.tooltip_manager
@ -353,16 +360,17 @@ impl Ui {
}
let mut retry = false;
self.maintain_internal(renderer, view_projection_mat, &mut retry);
self.maintain_internal(renderer, pool, view_projection_mat, &mut retry);
if retry {
// Update the glyph cache and try again.
self.maintain_internal(renderer, view_projection_mat, &mut retry);
self.maintain_internal(renderer, pool, view_projection_mat, &mut retry);
}
}
fn maintain_internal(
&mut self,
renderer: &mut Renderer,
pool: Option<&SlowJobPool>,
view_projection_mat: Option<Mat4<f32>>,
retry: &mut bool,
) {
@ -806,6 +814,7 @@ impl Ui {
// Cache graphic at particular resolution.
let (uv_aabr, tex_id) = match graphic_cache.cache_res(
renderer,
pool,
*graphic_id,
resolution,
source_aabr,

View File

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