diff --git a/src/core/mod.rs b/src/core/mod.rs index 0e77768..e3f2ea3 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -4,6 +4,7 @@ mod common; mod metadata_writer; mod region_group; mod region_processor; +mod tile_collector; mod tile_mipmapper; mod tile_renderer; diff --git a/src/core/tile_collector.rs b/src/core/tile_collector.rs new file mode 100644 index 0000000..dfc6085 --- /dev/null +++ b/src/core/tile_collector.rs @@ -0,0 +1,107 @@ +//! A trait for recursively processing tiles +//! +//! Used for mipmap generation and collecting entity data + +use std::sync::mpsc; + +use anyhow::Result; +use rayon::prelude::*; + +use super::common::*; + +/// Helper to determine if no further mipmap levels are needed +/// +/// If all tile coordinates are -1 or 0, further mipmap levels will not +/// decrease the number of tiles and mipmap generated is considered finished. +fn done(tiles: &TileCoordMap) -> bool { + tiles + .0 + .iter() + .all(|(z, xs)| (-1..=0).contains(z) && xs.iter().all(|x| (-1..=0).contains(x))) +} + +/// Derives the map of populated tile coordinates for the next mipmap level +fn map_coords(tiles: &TileCoordMap) -> TileCoordMap { + let mut ret = TileCoordMap::default(); + + for (&z, xs) in &tiles.0 { + for &x in xs { + let xt = x >> 1; + let zt = z >> 1; + + ret.0.entry(zt).or_default().insert(xt); + } + } + + ret +} + +/// Trait to implement for collecting tiles recursively +pub trait TileCollector: Sync { + /// Return value of [TileCollector::collect_one] + type CollectOutput: Send; + + /// List of level 0 tiles + fn tiles(&self) -> &[TileCoords]; + + /// Called at the beginning of each level of processing + fn prepare(&self, level: usize) -> Result<()>; + + /// Called at the end of each level of processing + fn finish( + &self, + level: usize, + outputs: impl Iterator, + ) -> Result<()>; + + /// Called for each tile coordinate of the level that is currently being generated + fn collect_one( + &self, + level: usize, + coords: TileCoords, + prev: &TileCoordMap, + ) -> Result; + + /// Collects tiles recursively + fn collect_tiles(&self) -> Result> { + let mut tile_stack = { + let mut tile_map = TileCoordMap::default(); + + for &TileCoords { x, z } in self.tiles() { + tile_map.0.entry(z).or_default().insert(x); + } + + vec![tile_map] + }; + + loop { + let level = tile_stack.len(); + let prev = &tile_stack[level - 1]; + if done(prev) { + break; + } + + self.prepare(level)?; + + let next = map_coords(prev); + + let (send, recv) = mpsc::channel(); + + next.0 + .par_iter() + .flat_map(|(&z, xs)| xs.par_iter().map(move |&x| TileCoords { x, z })) + .try_for_each(|coords| { + let output = self.collect_one(level, coords, prev)?; + send.send(output).unwrap(); + anyhow::Ok(()) + })?; + + drop(send); + self.finish(level, recv.into_iter())?; + + tile_stack.push(next); + } + + Ok(tile_stack) + } +} diff --git a/src/core/tile_mipmapper.rs b/src/core/tile_mipmapper.rs index 6ea8750..a372ca5 100644 --- a/src/core/tile_mipmapper.rs +++ b/src/core/tile_mipmapper.rs @@ -1,14 +1,53 @@ //! The [TileMipmapper] -use std::sync::mpsc; +use std::ops::Add; use anyhow::{Context, Result}; -use rayon::prelude::*; use tracing::{debug, info, warn}; -use super::common::*; +use super::{common::*, tile_collector::TileCollector}; use crate::{io::fs, types::*}; +/// Counters for the number of processed and total tiles +/// +/// Used as return of [TileMipmapper::collect_one] +#[derive(Debug, Clone, Copy)] +pub struct MipmapStat { + /// Total number of tiles + total: usize, + /// Processed number of tiles + processed: usize, +} + +impl MipmapStat { + /// Mipmap step return when none of the input files exist + const NOT_FOUND: MipmapStat = MipmapStat { + total: 0, + processed: 0, + }; + /// Mipmap step return when output file is up-to-date + const SKIPPED: MipmapStat = MipmapStat { + total: 1, + processed: 0, + }; + /// Mipmap step return when a new output file has been generated + const PROCESSED: MipmapStat = MipmapStat { + total: 1, + processed: 1, + }; +} + +impl Add for MipmapStat { + type Output = MipmapStat; + + fn add(self, rhs: Self) -> Self::Output { + MipmapStat { + total: self.total + rhs.total, + processed: self.processed + rhs.processed, + } + } +} + /// Generates mipmap tiles from full-resolution tile images pub struct TileMipmapper<'a> { /// Common MinedMap configuration from command line @@ -17,39 +56,63 @@ pub struct TileMipmapper<'a> { regions: &'a [TileCoords], } +impl<'a> TileCollector for TileMipmapper<'a> { + type CollectOutput = MipmapStat; + + fn tiles(&self) -> &[TileCoords] { + self.regions + } + + fn prepare(&self, level: usize) -> Result<()> { + info!("Generating level {} mipmaps...", level); + + fs::create_dir_all(&self.config.tile_dir(TileKind::Map, level))?; + fs::create_dir_all(&self.config.tile_dir(TileKind::Lightmap, level))?; + + Ok(()) + } + + fn finish( + &self, + level: usize, + outputs: impl Iterator, + ) -> Result<()> { + let stat = outputs.fold( + MipmapStat { + total: 0, + processed: 0, + }, + MipmapStat::add, + ); + info!( + "Generated level {} mipmaps ({} processed, {} unchanged)", + level, + stat.processed, + stat.total - stat.processed, + ); + + Ok(()) + } + + fn collect_one( + &self, + level: usize, + coords: TileCoords, + prev: &TileCoordMap, + ) -> Result { + let map_stat = self.render_mipmap::>(TileKind::Map, level, coords, prev)?; + let lightmap_stat = + self.render_mipmap::>(TileKind::Lightmap, level, coords, prev)?; + Ok(map_stat + lightmap_stat) + } +} + impl<'a> TileMipmapper<'a> { /// Constructs a new TileMipmapper pub fn new(config: &'a Config, regions: &'a [TileCoords]) -> Self { TileMipmapper { config, regions } } - /// Helper to determine if no further mipmap levels are needed - /// - /// If all tile coordinates are -1 or 0, further mipmap levels will not - /// decrease the number of tiles and mipmap generated is considered finished. - fn done(tiles: &TileCoordMap) -> bool { - tiles - .0 - .iter() - .all(|(z, xs)| (-1..=0).contains(z) && xs.iter().all(|x| (-1..=0).contains(x))) - } - - /// Derives the map of populated tile coordinates for the next mipmap level - fn map_coords(tiles: &TileCoordMap) -> TileCoordMap { - let mut ret = TileCoordMap::default(); - - for (&z, xs) in &tiles.0 { - for &x in xs { - let xt = x >> 1; - let zt = z >> 1; - - ret.0.entry(zt).or_default().insert(xt); - } - } - - ret - } - /// Renders and saves a single mipmap tile image /// /// Each mipmap tile is rendered by taking 2x2 tiles from the @@ -60,9 +123,7 @@ impl<'a> TileMipmapper<'a> { level: usize, coords: TileCoords, prev: &TileCoordMap, - count_total: &mpsc::Sender<()>, - count_processed: &mpsc::Sender<()>, - ) -> Result<()> + ) -> Result where [P::Subpixel]: image::EncodableLayout, image::ImageBuffer>: Into, @@ -97,11 +158,9 @@ impl<'a> TileMipmapper<'a> { .collect(); let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else { - return Ok(()); + return Ok(MipmapStat::NOT_FOUND); }; - count_total.send(()).unwrap(); - if Some(input_timestamp) <= output_timestamp { debug!( "Skipping unchanged mipmap tile {}", @@ -110,7 +169,7 @@ impl<'a> TileMipmapper<'a> { .expect("tile path must be in output directory") .display(), ); - return Ok(()); + return Ok(MipmapStat::SKIPPED); } debug!( @@ -156,79 +215,11 @@ impl<'a> TileMipmapper<'a> { }, )?; - count_processed.send(()).unwrap(); - Ok(()) + Ok(MipmapStat::PROCESSED) } /// Runs the mipmap generation pub fn run(self) -> Result> { - let mut tile_stack = { - let mut tile_map = TileCoordMap::default(); - - for &TileCoords { x, z } in self.regions { - tile_map.0.entry(z).or_default().insert(x); - } - - vec![tile_map] - }; - - loop { - let level = tile_stack.len(); - let prev = &tile_stack[level - 1]; - if Self::done(prev) { - break; - } - - info!("Generating level {} mipmaps...", level); - - fs::create_dir_all(&self.config.tile_dir(TileKind::Map, level))?; - fs::create_dir_all(&self.config.tile_dir(TileKind::Lightmap, level))?; - - let next = Self::map_coords(prev); - - let (total_send, total_recv) = mpsc::channel(); - let (processed_send, processed_recv) = mpsc::channel(); - - next.0 - .par_iter() - .flat_map(|(&z, xs)| xs.par_iter().map(move |&x| TileCoords { x, z })) - .try_for_each(|coords| { - self.render_mipmap::>( - TileKind::Map, - level, - coords, - prev, - &total_send, - &processed_send, - )?; - self.render_mipmap::>( - TileKind::Lightmap, - level, - coords, - prev, - &total_send, - &processed_send, - )?; - - anyhow::Ok(()) - })?; - - drop(total_send); - let total = total_recv.into_iter().count(); - - drop(processed_send); - let processed = processed_recv.into_iter().count(); - - info!( - "Generated level {} mipmaps ({} processed, {} unchanged)", - level, - processed, - total - processed, - ); - - tile_stack.push(next); - } - - Ok(tile_stack) + self.collect_tiles() } }