From b1f5fc01dbc22452a35b0f1ddaa98acf908597a5 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 19 Jun 2022 11:10:38 -0400 Subject: [PATCH] Before removing variable size edge tiles --- Cargo.lock | 13 +- voxygen/Cargo.toml | 6 +- voxygen/src/main.rs | 5 +- voxygen/src/mesh/greedy.rs | 375 +++++++++++++++++++++++++++++++---- voxygen/src/mesh/segment.rs | 2 +- voxygen/src/mesh/terrain.rs | 2 +- voxygen/src/scene/terrain.rs | 15 +- 7 files changed, 373 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8af4c60617..c16286a6a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1746,6 +1746,16 @@ dependencies = [ "str-buf", ] +[[package]] +name = "etagere" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6301151a318f367f392c31395beb1cfba5ccd9abc44d1db0db3a4b27b9601c89" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "euc" version = "0.5.3" @@ -2411,8 +2421,6 @@ dependencies = [ [[package]] name = "guillotiere" version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" dependencies = [ "euclid", "svg_fmt", @@ -6688,6 +6696,7 @@ dependencies = [ "egui_wgpu_backend", "egui_winit_platform", "enum-iterator", + "etagere", "euc", "gilrs", "glyph_brush", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 148ec677ea..48bd4a6906 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -103,7 +103,9 @@ crossbeam-channel = "0.5" directories-next = "2.0" dot_vox = "4.0" enum-iterator = "0.7" -guillotiere = "0.6" +# guillotiere = "0.6.2" +guillotiere = { path = "../../guillotiere"} +etagere = "0.2.7" hashbrown = {version = "0.11", features = ["rayon", "serde", "nightly"]} image = {version = "0.24", default-features = false, features = ["ico", "png"]} lazy_static = "1.4.0" @@ -122,7 +124,7 @@ treeculler = "0.2" tokio = { version = "1.14", default-features = false, features = ["rt-multi-thread"] } num_cpus = "1.0" # vec_map = { version = "0.8.2" } -inline_tweak = "1.0.2" +inline_tweak = {version = "1.0.2", features = ["release_tweak"]} itertools = "0.10.0" # Tracy diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index f7e57e9a9b..8f49250162 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -268,7 +268,10 @@ fn main() { let clipboard = iced_winit::Clipboard::connect(window.window()); - let lazy_init = SpriteRenderContext::new(window.renderer_mut()); + let mut lazy_init = SpriteRenderContext::new(window.renderer_mut()); + let _t = lazy_init(window.renderer_mut()); + std::thread::sleep(std::time::Duration::from_millis(100)); + std::process::exit(0); #[cfg(feature = "egui-ui")] let egui_state = EguiState::new(&window); diff --git a/voxygen/src/mesh/greedy.rs b/voxygen/src/mesh/greedy.rs index ef8f4f5014..de1d58b352 100644 --- a/voxygen/src/mesh/greedy.rs +++ b/voxygen/src/mesh/greedy.rs @@ -1,5 +1,5 @@ use crate::render::{mesh::Quad, ColLightInfo, TerrainVertex, Vertex}; -use common_base::span; +use common_base::{prof_span, span}; use vek::*; type TodoRect = ( @@ -77,17 +77,331 @@ pub struct GreedyConfig { /// vector. pub type SuspendedMesh<'a> = dyn for<'r> FnOnce(&'r mut ColLightInfo) + 'a; +/// Abstraction over different atlas allocators. Useful to swap out the +/// allocator implementation for specific cases (e.g. sprites). +pub trait AtlasAllocator { + //type Rect; + // TODO: add as parameter to `with_max_size`, also we may want to experiment + // with each use case: terrain, figures, etc to find the optimal config + //type Config; + + /// Creates a new instance of this atlas allocator taking into account the + /// provided max size; + fn with_max_size(max_size: Vec2) -> Self; + + /// Allocates a rectangle of the given size. + // TODO: don't use guillotiere type here + fn allocate(&mut self, size: Vec2) -> Option; + + /// Retrieves the current size of the atlas being allocated from. + fn size(&self) -> Vec2; + + /// Grows the size of the atlas to the provided size. + fn grow(&mut self, new_size: Vec2); +} + +fn guillotiere_size(size: Vec2) -> guillotiere::Size { guillotiere::Size::new(size.x, size.y) } + +impl AtlasAllocator for guillotiere::SimpleAtlasAllocator { + fn with_max_size(max_size: Vec2) -> Self { + // TODO: Collect information to see if we can choose a good value here. These + // current values were optimized for sprites, but we are using a + // different allocator for them so different values might be better + // here. + let large_size_threshold = inline_tweak::release_tweak!(8); //256.min(min_max_dim / 2 + 1); + let small_size_threshold = inline_tweak::release_tweak!(3); //33.min(large_size_threshold / 2 + 1); + // (12, 3) 24.5 + // (12, 2) 33.2 + // (12, 4) 27.2 + // (14, 3) 25.6 + // (10, 3) 20.9 + // (8, 3) 17.9 + // (6, 3) 18.0 + // (5, 3) 18.0 + let size = guillotiere_size(Vec2::new(32, 32)).min(guillotiere_size(max_size)); + + guillotiere::SimpleAtlasAllocator::with_options(size, &guillotiere::AllocatorOptions { + alignment: guillotiere::Size::new(1, 1), + small_size_threshold, + large_size_threshold, + }) + } + + /// Allocates a rectangle of the given size. + fn allocate(&mut self, size: Vec2) -> Option { + self.allocate(guillotiere_size(size)) + } + + /// Retrieves the current size of the atlas being allocated from. + fn size(&self) -> Vec2 { self.size().to_array().into() } + + /// Grows the size of the atlas to the provided size. + fn grow(&mut self, new_size: Vec2) { + { + prof_span!("debug"); + let free_space = self.free_space(); + //let (lw, lh) = self.largest_free_size(); + let area = self.size().width * self.size().height; // - lw * lh; + let used = area - free_space; + dbg!((used, free_space, area, area as f32 / used as f32,)); + } + self.grow(guillotiere_size(new_size)) + } +} + +fn etagere_size(size: Vec2) -> etagere::Size { etagere::Size::new(size.x, size.y) } + +// TODO: replace with constant after testing +fn tile_size() -> u16 { inline_tweak::release_tweak!(64) } + +pub struct GuillotiereTiled { + // TODO: Try BucketsAtlasAllocator + // Each tile is tile_size() (unless max size is not aligned to this, in which case the tiles + // that reach the max size are truncated below this value). + allocator: guillotiere::SimpleAtlasAllocator, + // (offset, size) + free_tiles: Vec<(Vec2, Vec2)>, + // Total width and height texels of the whole grid of tiles (in case this isn't a square). + // Not zero + size: Vec2, + // Offset (in texels) of current tile being allocated from (others returned `None` on last + // allocation attempt) + current: Option>, + // Efficiency history (total area, used area) + history: Vec<(i32, i32)>, +} + +impl GuillotiereTiled { + fn allocator_options() -> guillotiere::AllocatorOptions { + // TODO: Collect information to see if we can choose a good value here. + let large_size_threshold = inline_tweak::release_tweak!(8); //256.min(min_max_dim / 2 + 1); + let small_size_threshold = inline_tweak::release_tweak!(3); //33.min(large_size_threshold / 2 + 1); + + // (12, 3) 24.5 + // (12, 2) 33.2 + // (12, 4) 27.2 + // (14, 3) 25.6 + // (10, 3) 20.9 + // (8, 3) 17.9 + // (6, 3) 18.0 + // (5, 3) 18.0 + + guillotiere::AllocatorOptions { + alignment: guillotiere::Size::new(1, 1), + small_size_threshold, + large_size_threshold, + } + } + + fn next_tile(&mut self) { + if self.current.is_some() { + prof_span!("stats"); + let free_space = self.allocator.free_space(); + let size = self.allocator.size(); + let area = size.width * size.height; + let used = area - free_space; + self.history.push((area, used)); + } + + self.current = if let Some((offset, size)) = self.free_tiles.pop() { + self.allocator.reset(guillotiere_size(size), &Self::AllocatorOptions); + Some(offset) + } else { + None + }; + } +} + +impl AtlasAllocator for GuillotiereTiled { + fn with_max_size(max_size: Vec2) -> Self { + let size = guillotiere_size(Vec2::broadcast(i32::from(tile_size()))) + .min(guillotiere_size(max_size)); + + let allocator = guillotiere::SimpleAtlasAllocator::with_options(size, &Self::allocator_options()) + + Self { + allocator, + free_tiles: Vec::new(), + width: 1, + current: Some(Vec2::new(0, 0)), + } + } + + /// Allocates a rectangle of the given size. + fn allocate(&mut self, size: Vec2) -> Option { + //prof_span!("allocate_tiled"); + //tracing::info!("size {}", size); + let size = guillotiere_size(size); + + while let Some(current) = self.current { + let index = current.x + current.y * self.width; + match self.allocator.allocate(size) { + Some(r) => { + let offset = guillotiere_size(self.offset(index)); + + let offset_rect = guillotiere::Rectangle { + min: r.min.add_size(&offset), + max: r.max.add_size(&offset), + }; + + return Some(offset_rect); + }, + None => { + self.current = self.free_tiles.pop(); + tracing::error!("next tile"); + }, + } + } + dbg!(size); + + None + } + + /// Retrieves the current size of the atlas being allocated from. + fn size(&self) -> Vec2 { self.size() } + + /// Grows the size of the atlas to the provided size. + fn grow(&mut self, new_size: Vec2) { + let print_efficiency = inline_tweak::release_tweak!(true); + if print_efficiency { + prof_span!("debug"); + tracing::error!("here"); + println!("Tile count: {}", self.tiles.len()); + let mut total_area = 0; + let mut total_used = 0; + for tile in self.tiles.iter() { + let free_space = tile.free_space(); + let area = tile.size().width * tile.size().height; + let used = area - free_space; + dbg!((used, free_space, area, area as f32 / used as f32)); + total_area += area; + total_used += used; + } + dbg!(total_area as f32 / total_used as f32); + } + let last_tile_size = self + .free_tiles + .first() + .map(|(_offset, size)| size.to_array()) + .unwrap_or_else(|| self.allocator.size().to_array()); + if last_tile_size != [i32::from(tile_size()); 2] { + dbg!(new_size); + dbg!(last_tile_size); + let tile_size = tile_size(); + // We want to avoid this as it would create many tiles of irregular sizes. + //tracing::error!( + panic!( + "Growing when last tile is non-multiple-of-{tile_size}. Max size was already \ + reached or growing by non-power-of-two increments" + ); + } + let diff = (new_size - self.size).map(|e| e.max(0)); + dbg!(self.size()); + dbg!(new_size); + dbg!(diff); + // TODO: as cast + let diff_tiles = diff.map(|e| e as usize / usize::from(tile_size())); + // TODO: test when remainder isn't 0 + let diff_remainder = diff.map(|e| e % i32::from(tile_size())); + let additional_tiles = diff_tiles.map2(diff_remainder, |tiles, rem| tiles + if rem != 0 { 1 } else { 0 }); + dbg!(diff_tiles); + dbg!(additional_tiles); + let old_dims = self.dimensions; + let old_remainder = old_dims.map(|e| e % usize::from(tile_size())); + self.dimensions += diff: + dbg!(self.dimensions); + // Insert new colums + for _ in 0.. { + let last = column == self.width - 1; + let width = if last && diff_remainder.x != 0 { + diff_remainder.x + } else { + i32::from(tile_size()) + }; + + for x in 0..diff_tiles.x { + self.free_tiles.push( + Self::new_tile(guillotiere_size(Vec2::broadcast(i32::from(tile_size())))), + ); + } + if diff_remainder.x != 0 { + let index = end_of_row + diff_tiles.x; + self.tiles.insert( + index, + Self::new_tile(guillotiere_size(Vec2::new(diff.x, i32::from(tile_size())))), + ) + } + } + if old_height % usize::from(tile_size()) != 0 { + let last = column == self.width - 1; + let width = if last && diff.x != 0 { + diff.x + } else { + i32::from(tile_size()) + }; + self.tiles + .push(Self::new_tile(guillotiere_size(Vec2::new(width, diff.y)))); + } + } + // Insert new rows + for _ in 0..diff_tiles.y { + for column in 0..self.width { + let last = column == self.width - 1; + let width = if last && diff_remainder.x != 0 { + diff_remainder.x + } else { + i32::from(tile_size()) + }; + self.tiles.push(Self::new_tile(guillotiere_size(Vec2::new( + width, + i32::from(tile_size()), + )))); + } + } + if diff_remainder.y != 0 { + for column in 0..self.width { + let last = column == self.width - 1; + let width = if last && diff.x != 0 { + diff.x + } else { + i32::from(tile_size()) + }; + self.tiles + .push(Self::new_tile(guillotiere_size(Vec2::new(width, diff.y)))); + } + } + + // Add new tiles to free tile list + for x in old_width..self.width { + for y in 0..old_height { + self.free_tiles.push(Vec2::new(x, y)); + } + } + for y in old_height..self.height() { + for x in 0..self.width { + self.free_tiles.push(Vec2::new(x, y)); + } + } + if self.current.is_none() { + self.current = self.free_tiles.pop(); + } + } +} + +pub type SpriteAtlasAllocator = GuillotiereTiled; + /// Shared state for a greedy mesh, potentially passed along to multiple models. /// /// For an explanation of why we want this, see `SuspendedMesh`. -pub struct GreedyMesh<'a> { - atlas: guillotiere::SimpleAtlasAllocator, +pub struct GreedyMesh<'a, Allocator: AtlasAllocator = guillotiere::SimpleAtlasAllocator> { + //atlas: guillotiere::SimpleAtlasAllocator, + atlas: Allocator, col_lights_size: Vec2, - max_size: guillotiere::Size, + max_size: Vec2, suspended: Vec>>, } -impl<'a> GreedyMesh<'a> { +impl<'a, Allocator: AtlasAllocator> GreedyMesh<'a, Allocator> { /// Construct a new greedy mesher. /// /// Takes as input the maximum allowable size of the texture atlas used to @@ -102,25 +416,19 @@ impl<'a> GreedyMesh<'a> { /// to have at least 2 bits of the normal; thus, it can only take up at /// most 30 bits total, meaning we are restricted to "only" at most 2^15 /// × 2^15 atlases even if the hardware supports larger ones. + // TODO: could we change i32 -> u16 here? + // TODO: use this: pub fn new(max_size: Vec2) -> Self { pub fn new(max_size: guillotiere::Size) -> Self { + let max_size = Vec2::new(max_size.width, max_size.height); span!(_guard, "new", "GreedyMesh::new"); - let min_max_dim = max_size.width.min(max_size.height); + let min_max_dim = max_size.reduce_min(); assert!( min_max_dim >= 4, "min_max_dim={:?} >= 4 ({:?}", min_max_dim, max_size ); - // TODO: Collect information to see if we can choose a good value here. - let large_size_threshold = 256.min(min_max_dim / 2 + 1); - let small_size_threshold = 33.min(large_size_threshold / 2 + 1); - let size = guillotiere::Size::new(32, 32).min(max_size); - let atlas = - guillotiere::SimpleAtlasAllocator::with_options(size, &guillotiere::AllocatorOptions { - alignment: guillotiere::Size::new(1, 1), - small_size_threshold, - large_size_threshold, - }); + let atlas = Allocator::with_max_size(max_size); let col_lights_size = Vec2::new(1, 1); Self { atlas, @@ -185,13 +493,14 @@ impl<'a> GreedyMesh<'a> { col_lights_info } - pub fn max_size(&self) -> guillotiere::Size { self.max_size } + // TODO: don't use guillotiere type here + pub fn max_size(&self) -> guillotiere::Size { guillotiere_size(self.max_size) } } -fn greedy_mesh<'a, M: PartialEq, D: 'a, FL, FG, FO, FS, FP, FT>( - atlas: &mut guillotiere::SimpleAtlasAllocator, +fn greedy_mesh<'a, M: PartialEq, D: 'a, FL, FG, FO, FS, FP, FT, Allocator: AtlasAllocator>( + atlas: &mut Allocator, col_lights_size: &mut Vec2, - max_size: guillotiere::Size, + max_size: Vec2, GreedyConfig { mut data, draw_delta, @@ -419,27 +728,26 @@ fn greedy_mesh_cross_section( }); } -fn add_to_atlas( - atlas: &mut guillotiere::SimpleAtlasAllocator, +fn add_to_atlas( + atlas: &mut Allocator, todo_rects: &mut Vec, pos: Vec3, uv: Vec2>, dim: Vec2, norm: Vec3, faces_forward: bool, - max_size: guillotiere::Size, + max_size: Vec2, cur_size: &mut Vec2, ) -> guillotiere::Rectangle { + //prof_span!("add_to_atlas"); // TODO: Check this conversion. - let atlas_rect; - loop { + let atlas_rect = loop { // NOTE: Conversion to i32 is safe because he x, y, and z dimensions for any // chunk index must fit in at least an i16 (lower for x and y, probably // lower for z). - let res = atlas.allocate(guillotiere::Size::new(dim.x as i32 + 1, dim.y as i32 + 1)); - if let Some(atlas_rect_) = res { - atlas_rect = atlas_rect_; - break; + let res = atlas.allocate(Vec2::new(dim.x as i32 + 1, dim.y as i32 + 1)); + if let Some(atlas_rect) = res { + break atlas_rect; } // Allocation failure. let current_size = atlas.size(); @@ -463,12 +771,11 @@ fn add_to_atlas( } // Otherwise, we haven't reached max size yet, so double the size (or reach the // max texture size) and try again. - let new_size = guillotiere::Size::new( - max_size.width.min(current_size.width.saturating_mul(2)), - max_size.height.min(current_size.height.saturating_mul(2)), - ); + let new_size = max_size.map2(current_size, |max, current| { + max.min(current.saturating_mul(2)) + }); atlas.grow(new_size); - } + }; // NOTE: Conversion is correct because our initial max size for the atlas was // a u16 and we never grew the atlas, meaning all valid coordinates within the // atlas also fit into a u16. diff --git a/voxygen/src/mesh/segment.rs b/voxygen/src/mesh/segment.rs index f2cc9485d9..2a0625704f 100644 --- a/voxygen/src/mesh/segment.rs +++ b/voxygen/src/mesh/segment.rs @@ -117,7 +117,7 @@ where pub fn generate_mesh_base_vol_sprite<'a: 'b, 'b, V: 'a>( vol: V, (greedy, opaque_mesh, vertical_stripes): ( - &'b mut GreedyMesh<'a>, + &'b mut GreedyMesh<'a, crate::mesh::greedy::SpriteAtlasAllocator>, &'b mut Mesh, bool, ), diff --git a/voxygen/src/mesh/terrain.rs b/voxygen/src/mesh/terrain.rs index 8e13ee8cce..c76f5fc245 100644 --- a/voxygen/src/mesh/terrain.rs +++ b/voxygen/src/mesh/terrain.rs @@ -389,7 +389,7 @@ pub fn generate_mesh<'a, V: RectRasterableVol + ReadVol + Debug + ' |atlas_pos, pos, norm, meta| TerrainVertex::new(atlas_pos, pos + mesh_delta, norm, meta); let create_transparent = |_atlas_pos, pos, norm| FluidVertex::new(pos + mesh_delta, norm); - let mut greedy = GreedyMesh::new(max_size); + let mut greedy = GreedyMesh::::new(max_size); let mut opaque_mesh = Mesh::new(); let mut fluid_mesh = Mesh::new(); greedy.push(GreedyConfig { diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index e179b1800e..73a63c51f7 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -4,7 +4,7 @@ pub use self::watcher::{BlocksOfInterest, FireplaceType, Interaction}; use crate::{ mesh::{ - greedy::GreedyMesh, + greedy::{GreedyMesh, SpriteAtlasAllocator}, segment::generate_mesh_base_vol_sprite, terrain::{generate_mesh, SUNLIGHT}, }, @@ -152,6 +152,7 @@ struct SpriteConfig { wind_sway: f32, } +// TODO: reduce llvm IR lines from this /// Configuration data for all sprite models. /// /// NOTE: Model is an asset path to the appropriate sprite .vox model. @@ -437,12 +438,13 @@ impl SpriteRenderContext { } let join_handle = std::thread::spawn(move || { + prof_span!("mesh all sprites"); // Load all the sprite config data. let sprite_config = Arc::::load_expect("voxygen.voxel.sprite_manifest").cloned(); let max_size = guillotiere::Size::new(max_texture_size as i32, max_texture_size as i32); - let mut greedy = GreedyMesh::new(max_size); + let mut greedy = GreedyMesh::::new(max_size); let mut sprite_mesh = Mesh::new(); // NOTE: Tracks the start vertex of the next model to be meshed. let sprite_data: HashMap<(SpriteKind, usize), _> = SpriteKind::into_enum_iter() @@ -484,7 +486,9 @@ impl SpriteRenderContext { scale } }); - move |greedy: &mut GreedyMesh, sprite_mesh: &mut Mesh| { + move |greedy: &mut GreedyMesh, + sprite_mesh: &mut Mesh| { + prof_span!("mesh sprite"); let lod_sprite_data = scaled.map(|lod_scale_orig| { let lod_scale = model_scale * if lod_scale_orig == 1.0 { @@ -531,7 +535,10 @@ impl SpriteRenderContext { .map(|f| f(&mut greedy, &mut sprite_mesh)) .collect(); - let sprite_col_lights = greedy.finalize(); + let sprite_col_lights = { + prof_span!("finalize"); + greedy.finalize() + }; SpriteWorkerResponse { sprite_config,