Implement watch mode

This commit is contained in:
Matthias Schiffer 2025-02-13 19:56:19 +01:00
parent 37126f69fc
commit c10e9e4902
Signed by: neocturne
GPG key ID: 16EF3F64CB201D9C
5 changed files with 292 additions and 16 deletions

View file

@ -135,6 +135,8 @@ pub enum TileKind {
pub struct Config {
/// Number of threads for parallel processing
pub num_threads: usize,
/// Number of threads for initial parallel processing
pub num_threads_initial: usize,
/// Path of input region directory
pub region_dir: PathBuf,
/// Path of input `level.dat` file
@ -169,6 +171,7 @@ impl Config {
Some(threads) => threads,
None => 1,
};
let num_threads_initial = args.jobs_initial.unwrap_or(num_threads);
let region_dir = [&args.input_dir, Path::new("region")].iter().collect();
let level_dat_path = [&args.input_dir, Path::new("level.dat")].iter().collect();
@ -189,6 +192,7 @@ impl Config {
Ok(Config {
num_threads,
num_threads_initial,
region_dir,
level_dat_path,
level_dat_old_path,

View file

@ -10,7 +10,12 @@ mod tile_merger;
mod tile_mipmapper;
mod tile_renderer;
use std::path::PathBuf;
use std::{
path::PathBuf,
sync::mpsc::{self, Receiver},
thread,
time::Duration,
};
use anyhow::{Context, Result};
use clap::Parser;
@ -18,9 +23,13 @@ use git_version::git_version;
use common::{Config, ImageFormat};
use metadata_writer::MetadataWriter;
use notify::{RecommendedWatcher, RecursiveMode, Watcher as _};
use rayon::ThreadPool;
use region_processor::RegionProcessor;
use tile_mipmapper::TileMipmapper;
use tile_renderer::TileRenderer;
use tokio::runtime::Runtime;
use tracing::{info, warn};
use self::entity_collector::EntityCollector;
@ -44,9 +53,26 @@ pub struct Args {
/// use one thread per logical CPU core.
#[arg(short, long)]
pub jobs: Option<usize>,
/// Number of parallel threads to use for initial processing
///
/// Passing this option only makes sense with --watch. The first run after
/// starting MinedMap will use as many parallel jobs as configured using
/// --job-initial, while subsequent regenerations of tiles will use the
/// the number configured using --jobs.
///
/// If not given, the value from the --jobs option is used.
#[arg(long)]
pub jobs_initial: Option<usize>,
/// Enable verbose messages
#[arg(short, long)]
pub verbose: bool,
/// Watch for file changes and regenerate tiles automatically instead of
/// exiting after generation
#[arg(long)]
pub watch: bool,
/// Minimum delay between map generation cycles in watch mode
#[arg(long, value_parser = humantime::parse_duration, default_value = "30s")]
pub watch_delay: Duration,
/// Format of generated map tiles
#[arg(long, value_enum, default_value_t)]
pub image_format: ImageFormat,
@ -74,14 +100,73 @@ pub struct Args {
pub output_dir: PathBuf,
}
/// Configures the Rayon thread pool for parallel processing
fn setup_threads(num_threads: usize) -> Result<()> {
/// Configures a Rayon thread pool for parallel processing
fn setup_threads(num_threads: usize) -> Result<ThreadPool> {
rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build_global()
.build()
.context("Failed to configure thread pool")
}
/// Runs all MinedMap generation steps, updating all tiles as needed
fn generate(config: &Config, rt: &Runtime) -> Result<()> {
let regions = RegionProcessor::new(config).run()?;
TileRenderer::new(config, rt, &regions).run()?;
let tiles = TileMipmapper::new(config, &regions).run()?;
EntityCollector::new(config, &regions).run()?;
MetadataWriter::new(config, &tiles).run()
}
/// Creates a file watcher for the
fn create_watcher(args: &Args) -> Result<(RecommendedWatcher, Receiver<()>)> {
let (tx, rx) = mpsc::sync_channel::<()>(1);
let mut watcher = notify::recommended_watcher(move |res| {
// Ignore errors - we already have a watch trigger queued if try_send() fails
let event: notify::Event = match res {
Ok(event) => event,
Err(err) => {
warn!("Watch error: {err}");
return;
}
};
let notify::EventKind::Modify(modify_kind) = event.kind else {
return;
};
if !matches!(
modify_kind,
notify::event::ModifyKind::Data(_)
| notify::event::ModifyKind::Name(notify::event::RenameMode::To)
) {
return;
}
if !event
.paths
.iter()
.any(|path| path.ends_with("level.dat") || path.extension() == Some("mcu".as_ref()))
{
return;
}
let _ = tx.try_send(());
})?;
watcher.watch(&args.input_dir, RecursiveMode::Recursive)?;
Ok((watcher, rx))
}
/// Watches the data directory for changes, returning when a change has happened
fn wait_watcher(args: &Args, watch_channel: &Receiver<()>) -> Result<()> {
info!("Watching for changes...");
let () = watch_channel
.recv()
.context("Failed to read watch event channel")?;
info!("Change detected.");
thread::sleep(args.watch_delay);
let _ = watch_channel.try_recv();
Ok(())
}
/// MinedMap CLI main function
pub fn cli() -> Result<()> {
let args = Args::parse();
@ -96,17 +181,26 @@ pub fn cli() -> Result<()> {
.with_target(false)
.init();
setup_threads(config.num_threads)?;
let mut pool = setup_threads(config.num_threads_initial)?;
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let regions = RegionProcessor::new(&config).run()?;
TileRenderer::new(&config, &rt, &regions).run()?;
let tiles = TileMipmapper::new(&config, &regions).run()?;
EntityCollector::new(&config, &regions).run()?;
MetadataWriter::new(&config, &tiles).run()?;
let watch = args.watch.then(|| create_watcher(&args)).transpose()?;
Ok(())
pool.install(|| generate(&config, &rt))?;
let Some((_watcher, watch_channel)) = watch else {
// watch mode disabled
return Ok(());
};
if config.num_threads != config.num_threads_initial {
pool = setup_threads(config.num_threads)?;
}
pool.install(move || loop {
wait_watcher(&args, &watch_channel)?;
generate(&config, &rt)?;
})
}