core: split TileMerger out of TileMipmapper

Reusable mipmap-style tile merging
This commit is contained in:
Matthias Schiffer 2023-11-26 02:28:11 +01:00
parent 7740ce0522
commit 0f308788ef
Signed by: neocturne
GPG key ID: 16EF3F64CB201D9C
3 changed files with 220 additions and 106 deletions

View file

@ -5,6 +5,7 @@ mod metadata_writer;
mod region_group; mod region_group;
mod region_processor; mod region_processor;
mod tile_collector; mod tile_collector;
mod tile_merger;
mod tile_mipmapper; mod tile_mipmapper;
mod tile_renderer; mod tile_renderer;

97
src/core/tile_merger.rs Normal file
View file

@ -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<File>, sources: &[Source]) -> Result<()>;
/// Generates a tile at given coordinates and mipmap level
fn merge_tiles(&self, level: usize, coords: TileCoords, prev: &TileCoordMap) -> Result<Stat> {
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)
}
}

View file

@ -1,11 +1,15 @@
//! The [TileMipmapper] //! The [TileMipmapper]
use std::ops::Add; use std::{marker::PhantomData, ops::Add};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use tracing::{debug, info, warn}; 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::*}; use crate::{io::fs, types::*};
/// Counters for the number of processed and total tiles /// Counters for the number of processed and total tiles
@ -19,22 +23,23 @@ pub struct MipmapStat {
processed: usize, processed: usize,
} }
impl MipmapStat { impl From<tile_merger::Stat> for MipmapStat {
/// Mipmap step return when none of the input files exist fn from(value: tile_merger::Stat) -> Self {
const NOT_FOUND: MipmapStat = MipmapStat { match value {
total: 0, tile_merger::Stat::NotFound => MipmapStat {
processed: 0, total: 0,
}; processed: 0,
/// Mipmap step return when output file is up-to-date },
const SKIPPED: MipmapStat = MipmapStat { tile_merger::Stat::Skipped => MipmapStat {
total: 1, total: 1,
processed: 0, processed: 0,
}; },
/// Mipmap step return when a new output file has been generated tile_merger::Stat::Regenerate => MipmapStat {
const PROCESSED: MipmapStat = MipmapStat { total: 1,
total: 1, processed: 1,
processed: 1, },
}; }
}
} }
impl Add for MipmapStat { 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<P>,
}
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<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
{
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<std::fs::File>,
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::<P, Vec<P::Subpixel>>::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 /// Generates mipmap tiles from full-resolution tile images
pub struct TileMipmapper<'a> { pub struct TileMipmapper<'a> {
/// Common MinedMap configuration from command line /// Common MinedMap configuration from command line
@ -128,94 +229,9 @@ impl<'a> TileMipmapper<'a> {
[P::Subpixel]: image::EncodableLayout, [P::Subpixel]: image::EncodableLayout,
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>, image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
{ {
/// Tile width/height let merger = MapMerger::<P>::new(self.config, kind);
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32; let ret = merger.merge_tiles(level, coords, prev)?;
Ok(ret.into())
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::<P, Vec<P::Subpixel>>::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)
} }
/// Runs the mipmap generation /// Runs the mipmap generation