diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index ef1af1728d..42c65e3e94 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -282,6 +282,11 @@ impl Renderer { model.update(&mut self.encoder, mesh, offset) } + /// Return the maximum supported texture size. + pub fn max_texture_size(&self) -> usize { + self.factory.get_capabilities().max_texture_size + } + /// Create a new texture from the provided image. pub fn create_texture( &mut self, diff --git a/voxygen/src/ui/cache.rs b/voxygen/src/ui/cache.rs index b111222768..ae88334fd3 100644 --- a/voxygen/src/ui/cache.rs +++ b/voxygen/src/ui/cache.rs @@ -6,6 +6,13 @@ use crate::{ use conrod_core::text::GlyphCache; use vek::*; +// Multiplied by current window size +const GLYPH_CACHE_SIZE: u16 = 1; +const GRAPHIC_CACHE_SIZE: u16 = 2; +// Glyph cache tolerances +const SCALE_TOLERANCE: f32 = 0.1; +const POSITION_TOLERANCE: f32 = 0.1; + pub struct Cache { glyph_cache: GlyphCache<'static>, glyph_cache_tex: Texture, @@ -17,17 +24,21 @@ pub struct Cache { impl Cache { pub fn new(renderer: &mut Renderer) -> Result { let (w, h) = renderer.get_resolution().into_tuple(); - const SCALE_TOLERANCE: f32 = 0.1; - const POSITION_TOLERANCE: f32 = 0.1; - let graphic_cache_dims = Vec2::new(w * 4, h * 4); + let max_texture_size = renderer.max_texture_size(); + + let graphic_cache_dims = + Vec2::new(w, h).map(|e| (e * GRAPHIC_CACHE_SIZE).min(max_texture_size as u16)); + let glyph_cache_dims = + Vec2::new(w, h).map(|e| (e * GLYPH_CACHE_SIZE).min(max_texture_size as u16)); + Ok(Self { glyph_cache: GlyphCache::builder() - .dimensions(w as u32, h as u32) + .dimensions(glyph_cache_dims.x as u32, glyph_cache_dims.y as u32) .scale_tolerance(SCALE_TOLERANCE) .position_tolerance(POSITION_TOLERANCE) .build(), - glyph_cache_tex: renderer.create_dynamic_texture((w, h).into())?, + glyph_cache_tex: renderer.create_dynamic_texture(glyph_cache_dims.map(|e| e as u16))?, graphic_cache: GraphicCache::new(graphic_cache_dims), graphic_cache_tex: renderer.create_dynamic_texture(graphic_cache_dims)?, }) @@ -47,8 +58,28 @@ impl Cache { pub fn add_graphic(&mut self, graphic: Graphic) -> GraphicId { self.graphic_cache.add_graphic(graphic) } - pub fn clear_graphic_cache(&mut self, renderer: &mut Renderer, new_size: Vec2) { - self.graphic_cache.clear_cache(new_size); - self.graphic_cache_tex = renderer.create_dynamic_texture(new_size).unwrap(); + // Resizes and clears the GraphicCache + pub fn resize_graphic_cache(&mut self, renderer: &mut Renderer) -> Result<(), Error> { + let max_texture_size = renderer.max_texture_size(); + let cache_dims = renderer + .get_resolution() + .map(|e| (e * GRAPHIC_CACHE_SIZE).min(max_texture_size as u16)); + self.graphic_cache.clear_cache(cache_dims); + self.graphic_cache_tex = renderer.create_dynamic_texture(cache_dims)?; + Ok(()) + } + // Resizes and clears the GlyphCache + pub fn resize_glyph_cache(&mut self, renderer: &mut Renderer) -> Result<(), Error> { + let max_texture_size = renderer.max_texture_size(); + let cache_dims = renderer + .get_resolution() + .map(|e| (e * GLYPH_CACHE_SIZE).min(max_texture_size as u16)); + self.glyph_cache = GlyphCache::builder() + .dimensions(cache_dims.x as u32, cache_dims.y as u32) + .scale_tolerance(SCALE_TOLERANCE) + .position_tolerance(POSITION_TOLERANCE) + .build(); + self.glyph_cache_tex = renderer.create_dynamic_texture(cache_dims.map(|e| e as u16))?; + Ok(()) } } diff --git a/voxygen/src/ui/graphic/graphic.rs b/voxygen/src/ui/graphic/graphic.rs index 3c2408230c..7a6bc224fe 100644 --- a/voxygen/src/ui/graphic/graphic.rs +++ b/voxygen/src/ui/graphic/graphic.rs @@ -1,10 +1,12 @@ use dot_vox::DotVoxData; use fnv::FnvHashMap; -use guillotiere::{size2, Allocation, AtlasAllocator}; -use image::DynamicImage; +use guillotiere::{size2, AllocId, Allocation, AtlasAllocator}; +use image::{DynamicImage, RgbaImage}; +use log::{error, warn}; use std::sync::Arc; use vek::*; +#[derive(Clone)] pub enum Graphic { Image(Arc), Voxel(Arc, Option), @@ -16,19 +18,39 @@ pub struct Id(u32); type Parameters = (Id, Vec2, Aabr); +pub struct CachedDetails { + // Id used by AtlasAllocator + alloc_id: AllocId, + // Last frame this was used on + frame: u32, + // Where in the cache texture this is + aabr: Aabr, +} + pub struct GraphicCache { - atlas: AtlasAllocator, graphic_map: FnvHashMap, - rect_map: FnvHashMap>, next_id: u32, + + atlas: AtlasAllocator, + cache_map: FnvHashMap, + // The current frame + current_frame: u32, + unused_entries_this_frame: Option>>, + + soft_cache: FnvHashMap, + transfer_ready: Vec<(Parameters, Aabr)>, } impl GraphicCache { pub fn new(size: Vec2) -> Self { Self { - atlas: AtlasAllocator::new(size2(i32::from(size.x), i32::from(size.y))), graphic_map: FnvHashMap::default(), - rect_map: FnvHashMap::default(), next_id: 0, + atlas: AtlasAllocator::new(size2(i32::from(size.x), i32::from(size.y))), + cache_map: FnvHashMap::default(), + current_frame: 0, + unused_entries_this_frame: None, + soft_cache: FnvHashMap::default(), + transfer_ready: Vec::new(), } } pub fn add_graphic(&mut self, graphic: Graphic) -> Id { @@ -44,77 +66,177 @@ impl GraphicCache { self.graphic_map.get(&id) } pub fn clear_cache(&mut self, new_size: Vec2) { - self.rect_map.clear(); + self.soft_cache.clear(); + self.transfer_ready.clear(); + self.cache_map.clear(); self.atlas = AtlasAllocator::new(size2(i32::from(new_size.x), i32::from(new_size.y))); } - pub fn cache_res( + + pub fn queue_res( &mut self, graphic_id: Id, dims: Vec2, source: Aabr, - mut cacher: F, - ) -> Option> + ) -> Option> { + let key = (graphic_id, dims, source.map(|e| e.to_bits())); // TODO: Replace this with rounded representation of source + + if let Some(details) = self.cache_map.get_mut(&key) { + // Update frame + details.frame = self.current_frame; + + Some(details.aabr) + } else { + // Create image if it doesn't already exist + if !self.soft_cache.contains_key(&key) { + self.soft_cache.insert( + key, + match self.graphic_map.get(&graphic_id) { + Some(Graphic::Blank) => return None, + // Render image at requested resolution + // TODO: Use source aabr. + Some(Graphic::Image(ref image)) => image + .resize_exact( + u32::from(dims.x), + u32::from(dims.y), + image::FilterType::Nearest, + ) + .to_rgba(), + Some(Graphic::Voxel(ref vox, min_samples)) => { + super::renderer::draw_vox(&vox.as_ref().into(), dims, *min_samples) + } + None => { + warn!("A graphic was requested via an id which is not in use"); + return None; + } + }, + ); + } + + let aabr_from_alloc_rect = |rect: guillotiere::Rectangle| { + let (min, max) = (rect.min, rect.max); + Aabr { + min: Vec2::new(min.x as u16, min.y as u16), + max: Vec2::new(max.x as u16, max.y as u16), + } + }; + + // Allocate rectangle. + let (alloc_id, aabr) = match self + .atlas + .allocate(size2(i32::from(dims.x), i32::from(dims.y))) + { + Some(Allocation { id, rectangle }) => (id, aabr_from_alloc_rect(rectangle)), + // Out of room. + // 1) Remove unused allocations + // TODO: Make more room. + // 2) Rearrange rectangles (see comments below) + // 3) Expand cache size + None => { + // 1) Remove unused allocations + if self.unused_entries_this_frame.is_none() { + self.unused_entries_this_frame = { + let mut unused = self + .cache_map + .iter() + .filter_map(|(key, details)| { + if details.frame < self.current_frame - 1 { + Some(Some((details.frame, *key))) + } else { + None + } + }) + .collect::>(); + unused + .sort_unstable_by(|a, b| a.map(|(f, _)| f).cmp(&b.map(|(f, _)| f))); + Some(unused) + }; + } + + let mut allocation = None; + // Fight the checker! + let current_frame = self.current_frame; + // Will always be Some + if let Some(ref mut unused_entries) = self.unused_entries_this_frame { + // Deallocate from oldest to newest + for key in unused_entries + .iter_mut() + .filter_map(|e| e.take().map(|(_, key)| key)) + { + // Check if still in cache map and it has not been used since the vec was built + if self + .cache_map + .get(&key) + .filter(|d| d.frame != current_frame) + .is_some() + { + if let Some(alloc_id) = + self.cache_map.remove(&key).map(|d| d.alloc_id) + { + // Deallocate + self.atlas.deallocate(alloc_id); + // Try to allocate + allocation = self + .atlas + .allocate(size2(i32::from(dims.x), i32::from(dims.y))); + } + } + } + // 2) Rearrange rectangles + // This needs to be done infrequently and be based on whether rectangles have been removed + // Maybe find a way to calculate whether there is a significant amount of fragmentation + // Or consider dropping the use of an atlas and moving to a hashmap of individual textures :/ + // if allocation.is_none() { + // + // } + } + + match allocation { + Some(Allocation { id, rectangle }) => (id, aabr_from_alloc_rect(rectangle)), + None => { + warn!("Can't find space for an image in the graphic cache"); + return None; + } + } + } + }; + self.transfer_ready.push((key, aabr)); + + // Insert area into map for retrieval. + self.cache_map.insert( + key, + CachedDetails { + alloc_id, + frame: self.current_frame, + aabr, + }, + ); + + Some(aabr) + } + } + + // Anything not queued since the last call to this will be removed if there is not enough space in the cache + pub fn cache_queued(&mut self, mut cacher: F) where F: FnMut(Aabr, &[[u8; 4]]), { - match self - .rect_map - .get(&(graphic_id, dims, source.map(|e| e.to_bits()))) // TODO: Replace this with rounded representation of source - { - Some(aabr) => Some(*aabr), - None => match self.graphic_map.get(&graphic_id) { - Some(graphic) => { - // Allocate rectangle. - let aabr = match self - .atlas - .allocate(size2(i32::from(dims.x), i32::from(dims.y))) - { - Some(Allocation { id: _, rectangle }) => { - let (min, max) = (rectangle.min, rectangle.max); - Aabr { - min: Vec2::new(min.x as u16, min.y as u16), - max: Vec2::new(max.x as u16, max.y as u16), - } - } - // Out of room. - // TODO: Make more room. - // 1) Expand cache size - // 2) Remove unused allocations - // 3) Rearrange rectangles - None => return None, - }; - - // Render image. - // TODO: Use source. - let data = match graphic { - Graphic::Image(ref image) => image - .resize_exact( - u32::from(aabr.size().w), - u32::from(aabr.size().h), - image::FilterType::Nearest, - ) - .to_rgba() - // TODO: might be a better way to do this - .pixels() - .map(|p| p.data) - .collect::>(), - Graphic::Voxel(ref vox, min_samples) => - super::renderer::draw_vox(&vox.as_ref().into(), aabr.size().into(), *min_samples), - Graphic::Blank => return None, - }; - - // Draw to allocated area. - cacher(aabr, &data); - - // Insert area into map for retrieval. - self.rect_map - .insert((graphic_id, dims, source.map(|e| e.to_bits())), aabr); - - // Return area. - Some(aabr) - } - None => None, - }, + // Cached queued + // TODO: combine nearby transfers + for (key, target_aarb) in self.transfer_ready.drain(..) { + if let Some(image) = self.soft_cache.get(&key) { + cacher( + target_aarb, + &image.pixels().map(|p| p.data).collect::>(), + ); + } else { + error!("Image queued for transfer to gpu cache but it doesn't exist (this should never occur)"); + } } + + // Increment frame + self.current_frame += 1; + + // Reset unused entries + self.unused_entries_this_frame = None; } } diff --git a/voxygen/src/ui/graphic/renderer.rs b/voxygen/src/ui/graphic/renderer.rs index 16883ef511..06efe2d609 100644 --- a/voxygen/src/ui/graphic/renderer.rs +++ b/voxygen/src/ui/graphic/renderer.rs @@ -11,19 +11,20 @@ struct Voxel { mvp: Mat4, } +// TODO: use norm or remove it #[derive(Copy, Clone)] struct Vert { pos: Vec3, col: Rgb, - norm: Vec3, + //norm: Vec3, ao_level: u8, } impl Vert { - fn new(pos: Vec3, col: Rgb, norm: Vec3, ao_level: u8) -> Self { + fn new(pos: Vec3, col: Rgb, _norm: Vec3, ao_level: u8) -> Self { Vert { pos, col, - norm, + //norm, ao_level, } } @@ -40,7 +41,7 @@ impl<'a> Pipeline for Voxel { Vert { pos, col, - norm: _, + //norm: _, ao_level, }: &Self::Vertex, ) -> ([f32; 3], Self::VsOut) { @@ -57,11 +58,7 @@ impl<'a> Pipeline for Voxel { } } -pub fn draw_vox( - segment: &Segment, - output_size: Vec2, - min_samples: Option, -) -> Vec<[u8; 4]> { +pub fn draw_vox(segment: &Segment, output_size: Vec2, min_samples: Option) -> RgbaImage { let scale = min_samples.map_or(1.0, |s| s as f32).sqrt().ceil() as usize; let dims = output_size.map(|e| e as usize * scale).into_array(); let mut color = Buffer2d::new(dims, [0; 4]); @@ -85,33 +82,29 @@ pub fn draw_vox( &mut depth, ); - if scale > 1 { - DynamicImage::ImageRgba8( - RgbaImage::from_vec( - dims[0] as u32, - dims[1] as u32, - color - .as_ref() - .iter() - .flatten() - .cloned() - .collect::>(), - ) - .unwrap(), + let image = DynamicImage::ImageRgba8( + RgbaImage::from_vec( + dims[0] as u32, + dims[1] as u32, + color + .as_ref() + .iter() + .flatten() + .cloned() + .collect::>(), ) - .resize_exact( + .unwrap(), + ); + if scale > 1 { + image.resize_exact( output_size.x as u32, output_size.y as u32, image::FilterType::Triangle, ) - .to_rgba() - .pixels() - .map(|p| p.data) - .collect::>() } else { - // TODO: Remove clone. - color.as_ref().to_vec() + image } + .to_rgba() } fn ao_level(side1: bool, corner: bool, side2: bool) -> u8 { diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index a40495ee89..faa5487c85 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -101,6 +101,8 @@ pub struct Ui { ingame_locals: Vec>, // Window size for updating scaling window_resized: Option>, + // Used to delay cache resizing until after current frame is drawn + need_cache_resize: bool, // Scaling of the ui scale: Scale, } @@ -122,6 +124,7 @@ impl Ui { default_globals: renderer.create_consts(&[Globals::default()])?, ingame_locals: Vec::new(), window_resized: None, + need_cache_resize: false, scale, }) } @@ -216,6 +219,15 @@ impl Ui { None => return, }; + if self.need_cache_resize { + // Resize graphic cache + self.cache.resize_graphic_cache(renderer).unwrap(); + // Resize glyph cache + self.cache.resize_glyph_cache(renderer).unwrap(); + + self.need_cache_resize = false; + } + self.draw_commands.clear(); let mut mesh = Mesh::new(); @@ -344,7 +356,7 @@ impl Ui { PrimitiveKind::Image { image_id, color, - source_rect, + source_rect: _, // TODO: <-- use this } => { let graphic_id = self .image_map @@ -391,35 +403,25 @@ impl Ui { max: Vec2::new(uv_r, uv_t), } }; + // TODO: get dims from graphic_cache (or have it return floats directly) let (cache_w, cache_h) = cache_tex.get_dimensions().map(|e| e as f32).into_tuple(); // Cache graphic at particular resolution. - let uv_aabr = match graphic_cache.cache_res( - *graphic_id, - resolution, - source_aabr, - |aabr, data| { - let offset = aabr.min.into_array(); - let size = aabr.size().into_array(); - if let Err(err) = renderer.update_texture(cache_tex, offset, size, data) - { - warn!("Failed to update texture: {:?}", err); - } - }, - ) { - Some(aabr) => Aabr { - min: Vec2::new( - aabr.min.x as f32 / cache_w, - aabr.max.y as f32 / cache_h, - ), - max: Vec2::new( - aabr.max.x as f32 / cache_w, - aabr.min.y as f32 / cache_h, - ), - }, - None => continue, - }; + let uv_aabr = + match graphic_cache.queue_res(*graphic_id, resolution, source_aabr) { + Some(aabr) => Aabr { + min: Vec2::new( + aabr.min.x as f32 / cache_w, + aabr.max.y as f32 / cache_h, + ), + max: Vec2::new( + aabr.max.x as f32 / cache_w, + aabr.min.y as f32 / cache_h, + ), + }, + None => continue, + }; mesh.push_quad(create_ui_quad(gl_aabr(rect), uv_aabr, color, UiMode::Image)); } @@ -630,22 +632,31 @@ impl Ui { .create_dynamic_model(mesh.vertices().len() * 4 / 3) .unwrap(); } - renderer.update_model(&self.model, &mesh, 0).unwrap(); // Update model with new mesh. + renderer.update_model(&self.model, &mesh, 0).unwrap(); + + // Move cached graphics to the gpu + let (graphic_cache, cache_tex) = self.cache.graphic_cache_mut_and_tex(); + graphic_cache.cache_queued(|aabr, data| { + let offset = aabr.min.into_array(); + let size = aabr.size().into_array(); + if let Err(err) = renderer.update_texture(cache_tex, offset, size, data) { + warn!("Failed to update texture: {:?}", err); + } + }); // Handle window resizing. if let Some(new_dims) = self.window_resized.take() { + let (old_w, old_h) = self.scale.scaled_window_size().into_tuple(); self.scale.window_resized(new_dims, renderer); let (w, h) = self.scale.scaled_window_size().into_tuple(); self.ui.handle_event(Input::Resize(w, h)); - let res = renderer.get_resolution(); // Avoid panic in graphic cache when minimizing. - if res.x > 0 && res.y > 0 { - self.cache - .clear_graphic_cache(renderer, renderer.get_resolution().map(|e| e * 4)); - } - // TODO: Probably need to resize glyph cache, see conrod's gfx backend for reference. + // Avoid resetting cache if window size didn't change + // Somewhat inefficient for elements that won't change size after a window resize + let res = renderer.get_resolution(); + self.need_cache_resize = res.x > 0 && res.y > 0 && !(old_w == w && old_h == h); } } diff --git a/voxygen/src/ui/scale.rs b/voxygen/src/ui/scale.rs index 9c096cf366..46c945fb52 100644 --- a/voxygen/src/ui/scale.rs +++ b/voxygen/src/ui/scale.rs @@ -48,10 +48,6 @@ impl Scale { pub fn scale_factor_physical(&self) -> f64 { self.scale_factor_logical() * self.dpi_factor } - // Get the dpi factor (ratio between physical and logical coordinates) - pub fn dpi_factor(&self) -> f64 { - self.dpi_factor - } // Updates internal window size (and/or dpi_factor). pub fn window_resized(&mut self, new_dims: Vec2, renderer: &Renderer) { self.dpi_factor = renderer.get_resolution().x as f64 / new_dims.x; diff --git a/voxygen/src/ui/widgets/ingame.rs b/voxygen/src/ui/widgets/ingame.rs index cc286f3657..a8a41c31c1 100644 --- a/voxygen/src/ui/widgets/ingame.rs +++ b/voxygen/src/ui/widgets/ingame.rs @@ -1,8 +1,5 @@ use conrod_core::{ - builder_methods, - position::Dimension, - widget::{self, Id}, - Position, Ui, UiCell, Widget, WidgetCommon, + builder_methods, position::Dimension, widget, Position, Ui, UiCell, Widget, WidgetCommon, }; use vek::*; @@ -18,12 +15,11 @@ pub trait Ingameable: Widget + Sized { fn prim_count(&self) -> usize; // Note this is not responsible for the 3d positioning // Only call this directly if using IngameAnchor - fn set_ingame(self, id: widget::Id, parent_id: Id, ui: &mut UiCell) -> Self::Event { + fn set_ingame(self, id: widget::Id, ui: &mut UiCell) -> Self::Event { self // should pass focus to the window if these are clicked // (they are not displayed where conrod thinks they are) .graphics_for(ui.window) - //.parent(parent_id) // TODO: Is this needed? .set(id, ui) } fn position_ingame(self, pos: Vec3) -> Ingame { @@ -104,7 +100,7 @@ impl Widget for Ingame { } fn update(self, args: widget::UpdateArgs) -> Self::Event { - let widget::UpdateArgs { id, state, ui, .. } = args; + let widget::UpdateArgs { state, ui, .. } = args; let Ingame { widget, parameters, .. } = self; @@ -116,19 +112,19 @@ impl Widget for Ingame { }); } - widget.set_ingame(state.id.unwrap(), id, ui) + widget.set_ingame(state.id.unwrap(), ui) } - fn default_x_position(&self, ui: &Ui) -> Position { + fn default_x_position(&self, _: &Ui) -> Position { Position::Absolute(0.0) } - fn default_y_position(&self, ui: &Ui) -> Position { + fn default_y_position(&self, _: &Ui) -> Position { Position::Absolute(0.0) } - fn default_x_dimension(&self, ui: &Ui) -> Dimension { + fn default_x_dimension(&self, _: &Ui) -> Dimension { Dimension::Absolute(1.0) } - fn default_y_dimension(&self, ui: &Ui) -> Dimension { + fn default_y_dimension(&self, _: &Ui) -> Dimension { Dimension::Absolute(1.0) } }