core: split TileCollector trait out of TileMipmapper

Make mipmap-style recursive processing of tiles reusable.
This commit is contained in:
Matthias Schiffer 2023-11-26 01:20:09 +01:00
parent 7297c03567
commit 825cf70e51
Signed by: neocturne
GPG key ID: 16EF3F64CB201D9C
3 changed files with 206 additions and 107 deletions

View file

@ -4,6 +4,7 @@ mod common;
mod metadata_writer;
mod region_group;
mod region_processor;
mod tile_collector;
mod tile_mipmapper;
mod tile_renderer;

107
src/core/tile_collector.rs Normal file
View file

@ -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<Item = Self::CollectOutput>,
) -> 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<Self::CollectOutput>;
/// Collects tiles recursively
fn collect_tiles(&self) -> Result<Vec<TileCoordMap>> {
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)
}
}

View file

@ -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<Item = Self::CollectOutput>,
) -> 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<Self::CollectOutput> {
let map_stat = self.render_mipmap::<image::Rgba<u8>>(TileKind::Map, level, coords, prev)?;
let lightmap_stat =
self.render_mipmap::<image::LumaA<u8>>(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<MipmapStat>
where
[P::Subpixel]: image::EncodableLayout,
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
@ -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<Vec<TileCoordMap>> {
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::<image::Rgba<u8>>(
TileKind::Map,
level,
coords,
prev,
&total_send,
&processed_send,
)?;
self.render_mipmap::<image::LumaA<u8>>(
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()
}
}