How Configuration Files Are Managed in the Brush Project: A Typed, Layered Approach
The Brush project manages configuration files through a hierarchy of clap and serde-enabled structs that aggregate settings from an optional args.txt file and command-line arguments, with CLI flags always taking precedence.
Managing training parameters for 3D Gaussian splatting requires a flexible system that works across local directories and virtual file systems. The ArthurBrussee/brush repository solves this by implementing a type-safe configuration management system that unifies file-based settings with runtime CLI overrides. This approach allows users to store baseline configurations in dataset folders while retaining granular control via command-line flags.
Typed Configuration Structs with Clap and Serde
Brush organizes its settings into specialized configuration structs that are simultaneously parsable as CLI arguments and serializable to disk. Each struct uses #[derive(Args, Serialize, Deserialize)] to enable this dual representation.
Process-Wide Options
The ProcessConfig struct in crates/brush-process/src/config.rs defines global settings like random seeds, evaluation intervals, and export paths:
#[derive(Clone, Args, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ProcessConfig {
#[arg(long, default_value = "42")] pub seed: u64,
#[arg(long, default_value = "0")] pub start_iter: u32,
#[arg(long, default_value = "1000")] pub eval_every: u32,
#[arg(long, default_value = "false")] pub eval_save_to_disk: bool,
#[arg(long, default_value = "5000")] pub export_every: u32,
#[arg(long, default_value = "./{dataset}_exports/")] pub export_path: String,
#[arg(long, default_value = "export_{iter}.ply")] pub export_name: String,
}
Source: [crates/brush-process/src/config.rs](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/config.rs#L4-L38)
Training and Dataset Configurations
Training-specific parameters live in crates/brush-train/src/config.rs within the TrainConfig struct, while dataset loading options reside in crates/brush-dataset/src/config.rs as LoadDataseConfig. Both follow the same pattern of deriving Parser, Serialize, and Deserialize.
The Unified TrainStreamConfig
Individual configuration structs are flattened into a single TrainStreamConfig struct, which serves as the canonical representation of all runtime settings:
#[derive(Parser, Clone, Serialize, Deserialize)]
pub struct TrainStreamConfig {
#[clap(flatten)] #[serde(flatten)] pub train_config: brush_train::config::TrainConfig,
#[clap(flatten)] #[serde(flatten)] pub model_config: brush_dataset::config::ModelConfig,
#[clap(flatten)] #[serde(flatten)] pub load_config: brush_dataset::config::LoadDataseConfig,
#[clap(flatten)] #[serde(flatten)] pub process_config: ProcessConfig,
#[clap(flatten)] #[serde(flatten)] pub rerun_config: brush_rerun::RerunConfig,
}
Source: [crates/brush-process/src/config.rs](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/config.rs#L40-L58)
Loading Configuration from args.txt
Brush reads baseline settings from an args.txt file located in the dataset root or virtual file system (VFS). The load_config_from_vfs function in crates/brush-process/src/args_file.rs handles this by treating the file content as CLI arguments:
pub async fn load_config_from_vfs(vfs: &BrushVfs) -> Option<TrainStreamConfig> {
// … check presence of args.txt …
let mut reader = vfs.reader_at_path(Path::new("args.txt")).await.ok()?;
let mut content = String::new();
reader.read_to_string(&mut content).await.ok()?;
let file_args = split_args_str(&content);
let mut all_args = vec!["brush".to_owned()];
all_args.extend(file_args);
TrainStreamConfig::try_parse_from(&all_args).ok()
}
Source: [crates/brush-process/src/args_file.rs](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs#L13-L48)
The file uses the same flag syntax as the CLI, such as --total-train-iters 5000 or --export-path "./custom/".
Merging File-Based and CLI Arguments
Configuration precedence follows a simple rule: CLI arguments always override file-based settings. The merge_configs function implements this by concatenating the argument vectors and re-parsing them through clap:
pub fn merge_configs(initial: &TrainStreamConfig,
cli: &TrainStreamConfig) -> TrainStreamConfig {
let initial_args = config_to_args(initial);
let cli_args = config_to_args(cli);
let mut all_args = vec!["brush".to_owned()];
for a in initial_args.iter().chain(cli_args.iter()) {
all_args.extend(a.split_whitespace().map(|s| s.to_owned()));
}
TrainStreamConfig::try_parse_from(&all_args).unwrap_or_else(|_| cli.clone())
}
Source: [crates/brush-process/src/args_file.rs](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs#L112-L138)
This approach ensures that users can maintain a baseline args.txt in their dataset folder while running quick experiments with ad-hoc CLI overrides.
Serializing Configurations Back to Disk
Brush supports round-tripping configurations back to disk via the config_to_args function. To keep files concise, it only serializes values that differ from the defaults:
pub fn config_to_args(config: &TrainStreamConfig) -> Vec<String> {
let config_json = serde_json::to_value(config).unwrap();
let default_json = serde_json::to_value(TrainStreamConfig::default()).unwrap();
// Walk the two JSON objects, output `--key value` if the value differs.
// Handles bools, strings, numbers and multi‑value arrays.
// …
}
Source: [crates/brush-process/src/args_file.rs](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs#L51-L88)
This allows the application to persist the current effective configuration back to args.txt after a training run, creating a reproducible record of the exact parameters used.
End-to-End Configuration Flow
The entry point in crates/brush-process/src/lib.rs orchestrates the loading sequence:
let vfs = BrushVfs::from_path(&dataset_dir).await?;
let file_cfg = load_config_from_vfs(&vfs).await.unwrap_or_default();
let cli_cfg = TrainStreamConfig::parse(); // clap parses the CLI
let cfg = merge_configs(&file_cfg, &cli_cfg);
Source: brush-process/src/lib.rs
This flow demonstrates how Brush abstracts configuration files through its virtual file system (BrushVfs in crates/brush-vfs/src/lib.rs), enabling the same code to operate on local directories, zip archives, or WASM file systems.
Summary
- Configuration files in Brush use typed structs that derive both
clap::Argsandserdetraits, enabling simultaneous CLI parsing and disk serialization. - The
TrainStreamConfigstruct aggregates all sub-configurations (training, dataset, process, and Rerun) into a single canonical representation. - Settings load from an optional
args.txtfile using the same syntax as CLI arguments, read through theBrushVfsabstraction. - CLI arguments override file settings via the
merge_configsfunction, which re-parses combined argument lists throughclap. - Round-tripping is supported through
config_to_args, which serializes only non-default values to maintain concise configuration files.
Frequently Asked Questions
Where does Brush store its configuration files?
Brush stores runtime settings in an args.txt file located in the dataset root directory or virtual file system root. This file contains CLI-style flags such as --total-train-iters 5000 and is read at startup by the load_config_from_vfs function in crates/brush-process/src/args_file.rs.
How does Brush handle configuration precedence between files and CLI?
Brush merges configurations by converting both the file-based settings and CLI arguments back into argument vectors, concatenating them, and re-parsing through clap. Because CLI arguments appear later in the sequence, they automatically override any duplicate keys from the args.txt file.
Can Brush serialize the current configuration back to disk?
Yes. The config_to_args function converts a TrainStreamConfig instance back into a vector of CLI arguments by comparing it against the default configuration using serde_json. It only emits flags for values that differ from defaults, keeping the resulting args.txt file minimal and human-readable.
What makes Brush's configuration system type-safe?
Each configuration field is defined as a strongly-typed struct member with explicit types like u64, u32, or String, combined with clap derive macros for validation. This ensures that invalid values in args.txt are caught at parse time, and the compiler enforces that all required parameters are present before training begins.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →