mirror of
https://github.com/neocturne/MinedMap.git
synced 2025-03-04 17:23:33 +01:00
core: split TileMerger out of TileMipmapper
Reusable mipmap-style tile merging
This commit is contained in:
parent
7740ce0522
commit
0f308788ef
3 changed files with 220 additions and 106 deletions
|
@ -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
97
src/core/tile_merger.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue