From 0f308788ef7b1579407b5a7f18b153179cf9bb7e Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Sun, 26 Nov 2023 02:28:11 +0100 Subject: [PATCH] core: split TileMerger out of TileMipmapper Reusable mipmap-style tile merging --- src/core/mod.rs | 1 + src/core/tile_merger.rs | 97 ++++++++++++++++ src/core/tile_mipmapper.rs | 228 ++++++++++++++++++++----------------- 3 files changed, 220 insertions(+), 106 deletions(-) create mode 100644 src/core/tile_merger.rs diff --git a/src/core/mod.rs b/src/core/mod.rs index e3f2ea3..f892a62 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -5,6 +5,7 @@ mod metadata_writer; mod region_group; mod region_processor; mod tile_collector; +mod tile_merger; mod tile_mipmapper; mod tile_renderer; diff --git a/src/core/tile_merger.rs b/src/core/tile_merger.rs new file mode 100644 index 0000000..3a14390 --- /dev/null +++ b/src/core/tile_merger.rs @@ -0,0 +1,97 @@ +//! Mipmap-style merging of tiles + +use std::{ + fs::File, + io::BufWriter, + path::{Path, PathBuf}, + time::SystemTime, +}; + +use anyhow::Result; +use tracing::warn; + +use super::common::*; +use crate::io::fs; + +/// [TileMerger::merge_tiles] return +#[derive(Debug, Clone, Copy)] +pub enum Stat { + /// None of the input files were found + NotFound, + /// The output file is up-to-date + Skipped, + /// The output file is regenerated + Regenerate, +} + +/// A source file for the [TileMerger] +/// +/// The tuple elements are X and Z coordinate offsets in the range [0, 1], +/// the file path and the time of last change of the input. +pub type Source = ((i32, i32), PathBuf, SystemTime); + +/// Reusable trait for mipmap-style tile merging with change tracking +pub trait TileMerger { + /// [fs::FileMetaVersion] of input and output files + /// + /// The version in the file metadata on disk must match the returned + /// version for the a to be considered up-to-date. + fn file_meta_version(&self) -> fs::FileMetaVersion; + + /// Returns the paths of input and output files + fn tile_path(&self, level: usize, coords: TileCoords) -> PathBuf; + + /// Can be used to log the processing status + fn log(&self, _output_path: &Path, _stat: Stat) {} + + /// Handles the actual merging of source files + fn write_tile(&self, file: &mut BufWriter, sources: &[Source]) -> Result<()>; + + /// Generates a tile at given coordinates and mipmap level + fn merge_tiles(&self, level: usize, coords: TileCoords, prev: &TileCoordMap) -> Result { + let version = self.file_meta_version(); + let output_path = self.tile_path(level, coords); + let output_timestamp = fs::read_timestamp(&output_path, version); + + let sources: Vec<_> = [(0, 0), (0, 1), (1, 0), (1, 1)] + .into_iter() + .filter_map(|(dx, dz)| { + let source_coords = TileCoords { + x: 2 * coords.x + dx, + z: 2 * coords.z + dz, + }; + if !prev.contains(source_coords) { + return None; + } + + let source_path = self.tile_path(level - 1, source_coords); + let timestamp = match fs::modified_timestamp(&source_path) { + Ok(timestamp) => timestamp, + Err(err) => { + warn!("{}", err); + return None; + } + }; + Some(((dx, dz), source_path, timestamp)) + }) + .collect(); + + let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else { + self.log(&output_path, Stat::NotFound); + return Ok(Stat::NotFound); + }; + + if Some(input_timestamp) <= output_timestamp { + self.log(&output_path, Stat::Skipped); + return Ok(Stat::Skipped); + } + + self.log(&output_path, Stat::Regenerate); + + fs::create_with_timestamp(&output_path, version, input_timestamp, |file| { + self.write_tile(file, &sources) + })?; + + Ok(Stat::Regenerate) + } +} diff --git a/src/core/tile_mipmapper.rs b/src/core/tile_mipmapper.rs index a372ca5..1873cf9 100644 --- a/src/core/tile_mipmapper.rs +++ b/src/core/tile_mipmapper.rs @@ -1,11 +1,15 @@ //! The [TileMipmapper] -use std::ops::Add; +use std::{marker::PhantomData, ops::Add}; use anyhow::{Context, Result}; use tracing::{debug, info, warn}; -use super::{common::*, tile_collector::TileCollector}; +use super::{ + common::*, + tile_collector::TileCollector, + tile_merger::{self, TileMerger}, +}; use crate::{io::fs, types::*}; /// Counters for the number of processed and total tiles @@ -19,22 +23,23 @@ pub struct MipmapStat { 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 From for MipmapStat { + fn from(value: tile_merger::Stat) -> Self { + match value { + tile_merger::Stat::NotFound => MipmapStat { + total: 0, + processed: 0, + }, + tile_merger::Stat::Skipped => MipmapStat { + total: 1, + processed: 0, + }, + tile_merger::Stat::Regenerate => MipmapStat { + total: 1, + processed: 1, + }, + } + } } impl Add for MipmapStat { @@ -48,6 +53,102 @@ impl Add for MipmapStat { } } +/// [TileMerger] for map tile images +struct MapMerger<'a, P> { + /// Common MinedMap configuration from command line + config: &'a Config, + /// Tile kind (map or lightmap) + kind: TileKind, + /// Pixel format type + _pixel: PhantomData

, +} + +impl<'a, P> MapMerger<'a, P> { + /// Creates a new [MapMerger] + fn new(config: &'a Config, kind: TileKind) -> Self { + MapMerger { + config, + kind, + _pixel: PhantomData, + } + } +} + +impl<'a, P: image::PixelWithColorType> TileMerger for MapMerger<'a, P> +where + [P::Subpixel]: image::EncodableLayout, + image::ImageBuffer>: Into, +{ + fn file_meta_version(&self) -> fs::FileMetaVersion { + MIPMAP_FILE_META_VERSION + } + + fn tile_path(&self, level: usize, coords: TileCoords) -> std::path::PathBuf { + self.config.tile_path(self.kind, level, coords) + } + + fn log(&self, output_path: &std::path::Path, stat: super::tile_merger::Stat) { + match stat { + super::tile_merger::Stat::NotFound => {} + super::tile_merger::Stat::Skipped => { + debug!( + "Skipping unchanged mipmap tile {}", + output_path + .strip_prefix(&self.config.output_dir) + .expect("tile path must be in output directory") + .display(), + ); + } + super::tile_merger::Stat::Regenerate => { + debug!( + "Rendering mipmap tile {}", + output_path + .strip_prefix(&self.config.output_dir) + .expect("tile path must be in output directory") + .display(), + ); + } + }; + } + + fn write_tile( + &self, + file: &mut std::io::BufWriter, + sources: &[super::tile_merger::Source], + ) -> Result<()> { + /// Tile width/height + const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32; + + let mut image: image::DynamicImage = + image::ImageBuffer::>::new(N, N).into(); + + for ((dx, dz), source_path, _) in sources { + let source = match image::open(source_path) { + Ok(source) => source, + Err(err) => { + warn!( + "Failed to read source image {}: {}", + source_path.display(), + err, + ); + continue; + } + }; + let resized = source.resize(N / 2, N / 2, image::imageops::FilterType::Triangle); + image::imageops::overlay( + &mut image, + &resized, + *dx as i64 * (N / 2) as i64, + *dz as i64 * (N / 2) as i64, + ); + } + + image + .write_to(file, image::ImageFormat::Png) + .context("Failed to save image") + } +} + /// Generates mipmap tiles from full-resolution tile images pub struct TileMipmapper<'a> { /// Common MinedMap configuration from command line @@ -128,94 +229,9 @@ impl<'a> TileMipmapper<'a> { [P::Subpixel]: image::EncodableLayout, image::ImageBuffer>: Into, { - /// Tile width/height - const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32; - - let output_path = self.config.tile_path(kind, level, coords); - let output_timestamp = fs::read_timestamp(&output_path, MIPMAP_FILE_META_VERSION); - - let sources: Vec<_> = [(0, 0), (0, 1), (1, 0), (1, 1)] - .into_iter() - .filter_map(|(dx, dz)| { - let source_coords = TileCoords { - x: 2 * coords.x + dx, - z: 2 * coords.z + dz, - }; - if !prev.contains(source_coords) { - return None; - } - - let source_path = self.config.tile_path(kind, level - 1, source_coords); - let timestamp = match fs::modified_timestamp(&source_path) { - Ok(timestamp) => timestamp, - Err(err) => { - warn!("{}", err); - return None; - } - }; - Some(((dx, dz), source_path, timestamp)) - }) - .collect(); - - let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else { - return Ok(MipmapStat::NOT_FOUND); - }; - - if Some(input_timestamp) <= output_timestamp { - debug!( - "Skipping unchanged mipmap tile {}", - output_path - .strip_prefix(&self.config.output_dir) - .expect("tile path must be in output directory") - .display(), - ); - return Ok(MipmapStat::SKIPPED); - } - - debug!( - "Rendering mipmap tile {}", - output_path - .strip_prefix(&self.config.output_dir) - .expect("tile path must be in output directory") - .display(), - ); - - let mut image: image::DynamicImage = - image::ImageBuffer::>::new(N, N).into(); - - for ((dx, dz), source_path, _) in sources { - let source = match image::open(&source_path) { - Ok(source) => source, - Err(err) => { - warn!( - "Failed to read source image {}: {}", - source_path.display(), - err, - ); - continue; - } - }; - let resized = source.resize(N / 2, N / 2, image::imageops::FilterType::Triangle); - image::imageops::overlay( - &mut image, - &resized, - dx as i64 * (N / 2) as i64, - dz as i64 * (N / 2) as i64, - ); - } - - fs::create_with_timestamp( - &output_path, - MIPMAP_FILE_META_VERSION, - input_timestamp, - |file| { - image - .write_to(file, image::ImageFormat::Png) - .context("Failed to save image") - }, - )?; - - Ok(MipmapStat::PROCESSED) + let merger = MapMerger::

::new(self.config, kind); + let ret = merger.merge_tiles(level, coords, prev)?; + Ok(ret.into()) } /// Runs the mipmap generation