Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ use crate::{
ColmapAccelerationStatus, EnginePaths, EngineStatus,
},
error::{Result, SplatError},
pipeline::runner::{PipelineResult, PipelineRunner},
pipeline::{
estimate::{estimate_runtime, RuntimeEstimate},
runner::{PipelineResult, PipelineRunner},
},
presets::Quality,
project::{
catalog::{self, AppSettings, ProjectOverview},
Expand Down Expand Up @@ -107,6 +110,7 @@ pub struct GaussianVideoExportResult {
pub struct ProbeAndPlan {
video: VideoInfo,
plan: FramePlan,
estimate: RuntimeEstimate,
}

fn paths_for_app(app: &tauri::AppHandle) -> EnginePaths {
Expand All @@ -129,9 +133,21 @@ pub async fn probe_and_plan(
path: String,
quality: Quality,
) -> std::result::Result<ProbeAndPlan, SplatError> {
let video = probe_video(&paths_for_app(&app).ffprobe, &PathBuf::from(path), None).await?;
let engine_paths = paths_for_app(&app);
let video = probe_video(&engine_paths.ffprobe, &PathBuf::from(path), None).await?;
let plan = UniformRatioFrameSelection.create_plan(&video, &quality.preset());
Ok(ProbeAndPlan { video, plan })
let acceleration = detect_colmap_acceleration(&engine_paths).await;
let total_vram_mb = acceleration
.device
.as_ref()
.and_then(|device| device.total_memory_mb);
let samples = catalog::runtime_samples().await;
let estimate = estimate_runtime(&video, &plan, quality, total_vram_mb, &samples);
Ok(ProbeAndPlan {
video,
plan,
estimate,
})
}

#[tauri::command]
Expand Down Expand Up @@ -224,6 +240,57 @@ pub async fn start_pipeline(
result
}

#[tauri::command]
pub async fn resume_pipeline(
app: tauri::AppHandle,
state: State<'_, PipelineController>,
telemetry: State<'_, TelemetryService>,
project_id: String,
) -> std::result::Result<PipelineResult, SplatError> {
let project_id = parse_project_id(&project_id)?;
let (_, metadata) = catalog::load_registered_project(project_id).await?;
let emitter = app.clone();
let started = Instant::now();
let telemetry_session = Arc::new(PipelineTelemetrySession::new(
telemetry.inner().clone(),
metadata.quality,
));
let event_telemetry = telemetry_session.clone();
let runner = Arc::new(PipelineRunner::new(paths_for_app(&app), move |event| {
event_telemetry.observe(&event);
let _ = emitter.emit("pipeline-event", event);
}));
{
let mut active = state.active.lock().await;
if active.is_some() {
return Err(SplatError::Process("已有任务正在运行".into()));
}
*active = Some(runner.clone());
}
telemetry_session.generation_started();
let result = runner.resume(project_id).await;
match &result {
Ok(output) => telemetry_session.generation_completed(
output.duration_ms,
output.input_images,
output.source_duration_seconds,
),
Err(error) => telemetry_session.generation_failed(error),
}
if let Err(error) = &result {
let stage = if matches!(error, SplatError::Cancelled) {
crate::pipeline::PipelineStage::Cancelled
} else {
crate::pipeline::PipelineStage::Failed
};
let mut event = crate::pipeline::PipelineEvent::mapped(stage, 1.0, error.to_string());
event.elapsed_ms = started.elapsed().as_millis() as u64;
let _ = app.emit("pipeline-event", event);
}
*state.active.lock().await = None;
result
}

#[tauri::command]
pub async fn cancel_pipeline(state: State<'_, PipelineController>) -> Result<()> {
if let Some(runner) = state.active.lock().await.as_ref() {
Expand Down
51 changes: 49 additions & 2 deletions src-tauri/src/engines/brush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub async fn train(
preset.brush_iterations.to_string().into(),
OsString::from("--max-resolution"),
preset.brush_max_resolution.to_string().into(),
OsString::from("--max-splats"),
preset.brush_max_splats.to_string().into(),
OsString::from("--sh-degree"),
preset.brush_sh_degree.to_string().into(),
OsString::from("--export-every"),
preset.brush_iterations.to_string().into(),
OsString::from("--export-path"),
Expand All @@ -53,9 +57,10 @@ pub async fn train(
})
.await?;
if !output.success {
let detail = process_error_detail(&output.stdout, &output.stderr);
return Err(SplatError::Process(format!(
"Brush 退出码 {:?}",
output.exit_code
"Brush 退出码 {:?}:{detail}",
output.exit_code,
)));
}
let candidate = if candidate.is_file() {
Expand All @@ -76,3 +81,45 @@ pub async fn train(
}
Ok(candidate)
}

fn process_error_detail(stdout: &str, stderr: &str) -> String {
stderr
.lines()
.chain(stdout.lines())
.rev()
.find(|line| !line.trim().is_empty())
.map(str::trim)
.unwrap_or("未提供错误详情")
.chars()
.take(500)
.collect()
}

pub fn is_out_of_memory(error: &SplatError) -> bool {
let normalized = error.to_string().to_ascii_lowercase();
[
"out of memory",
"outofmemory",
"buffer too big",
"buffertoobig",
"allocation failed",
"device lost",
]
.iter()
.any(|needle| normalized.contains(needle))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn recognizes_common_webgpu_oom_messages() {
assert!(is_out_of_memory(&SplatError::Process(
"Brush: BufferTooBig while allocating".into()
)));
assert!(!is_out_of_memory(&SplatError::Process(
"invalid dataset".into()
)));
}
}
85 changes: 75 additions & 10 deletions src-tauri/src/engines/colmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ async fn matching_gpu_options(
executable: &Path,
manager: &ProcessManager,
) -> Result<(&'static str, &'static str)> {
let help = command_help(executable, "sequential_matcher", manager).await?;
let help = command_help(executable, "exhaustive_matcher", manager).await?;
if help.contains("--FeatureMatching.use_gpu") {
Ok(("--FeatureMatching.use_gpu", "--FeatureMatching.gpu_index"))
} else if help.contains("--SiftMatching.use_gpu") {
Ok(("--SiftMatching.use_gpu", "--SiftMatching.gpu_index"))
} else {
Err(SplatError::UnsupportedEngine(
"COLMAP sequential_matcher 不支持已知的 SIFT GPU 参数".into(),
"COLMAP exhaustive_matcher 不支持已知的 SIFT GPU 参数".into(),
))
}
}
Expand Down Expand Up @@ -158,7 +158,7 @@ pub async fn extract_features(
.await
}

pub async fn match_sequential(
pub async fn match_exhaustive(
executable: &Path,
database: &Path,
log: PathBuf,
Expand All @@ -169,7 +169,7 @@ pub async fn match_sequential(
let (use_gpu_option, gpu_index_option) = matching_gpu_options(executable, manager).await?;
run_colmap(
executable,
sequential_matching_args(database, gpu_index, use_gpu_option, gpu_index_option),
exhaustive_matching_args(database, gpu_index, use_gpu_option, gpu_index_option),
database.parent().unwrap_or(Path::new(".")),
log,
manager,
Expand Down Expand Up @@ -205,14 +205,14 @@ fn feature_extraction_args(
args
}

fn sequential_matching_args(
fn exhaustive_matching_args(
database: &Path,
gpu_index: Option<u32>,
use_gpu_option: &str,
gpu_index_option: &str,
) -> Vec<OsString> {
let mut args = vec![
"sequential_matcher".into(),
"exhaustive_matcher".into(),
"--database_path".into(),
database.into(),
use_gpu_option.into(),
Expand All @@ -223,8 +223,8 @@ fn sequential_matching_args(
args.push(index.to_string().into());
}
args.extend([
OsString::from("--SequentialMatching.overlap"),
OsString::from("10"),
OsString::from("--FeatureMatching.guided_matching"),
OsString::from("1"),
]);
args
}
Expand All @@ -237,6 +237,18 @@ pub async fn map(
log: PathBuf,
manager: &ProcessManager,
observer: Option<ProcessObserver>,
) -> Result<()> {
map_incremental(executable, database, images, output, log, manager, observer).await
}

pub async fn map_incremental(
executable: &Path,
database: &Path,
images: &Path,
output: &Path,
log: PathBuf,
manager: &ProcessManager,
observer: Option<ProcessObserver>,
) -> Result<()> {
tokio::fs::create_dir_all(output).await?;
run_colmap(
Expand All @@ -258,6 +270,56 @@ pub async fn map(
.await
}

#[allow(clippy::too_many_arguments)]
pub async fn map_global(
executable: &Path,
database: &Path,
images: &Path,
output: &Path,
log: PathBuf,
manager: &ProcessManager,
observer: Option<ProcessObserver>,
gpu_index: Option<u32>,
) -> Result<()> {
let help = command_help(executable, "global_mapper", manager).await?;
if !help.contains("--GlobalMapper.gp_use_gpu") {
return Err(SplatError::UnsupportedEngine(
"当前 COLMAP 不包含 Global Mapper".into(),
));
}
tokio::fs::create_dir_all(output).await?;
let mut args = vec![
"global_mapper".into(),
"--database_path".into(),
database.into(),
"--image_path".into(),
images.into(),
"--output_path".into(),
output.into(),
"--GlobalMapper.gp_use_gpu".into(),
(if gpu_index.is_some() { "1" } else { "0" }).into(),
"--GlobalMapper.ba_ceres_use_gpu".into(),
(if gpu_index.is_some() { "1" } else { "0" }).into(),
];
if let Some(index) = gpu_index {
args.extend([
"--GlobalMapper.gp_gpu_index".into(),
index.to_string().into(),
"--GlobalMapper.ba_ceres_gpu_index".into(),
index.to_string().into(),
]);
}
run_colmap(
executable,
args,
database.parent().unwrap_or(output),
log,
manager,
observer,
)
.await
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -284,7 +346,7 @@ mod tests {
.windows(2)
.any(|pair| pair == ["--FeatureExtraction.gpu_index", "2"]));

let matching = strings(sequential_matching_args(
let matching = strings(exhaustive_matching_args(
Path::new("database.db"),
Some(2),
"--FeatureMatching.use_gpu",
Expand All @@ -296,6 +358,9 @@ mod tests {
assert!(matching
.windows(2)
.any(|pair| pair == ["--FeatureMatching.gpu_index", "2"]));
assert!(matching
.windows(2)
.any(|pair| pair == ["--FeatureMatching.guided_matching", "1"]));
}

#[test]
Expand All @@ -314,7 +379,7 @@ mod tests {
.iter()
.any(|arg| arg == "--FeatureExtraction.gpu_index"));

let matching = strings(sequential_matching_args(
let matching = strings(exhaustive_matching_args(
Path::new("database.db"),
None,
"--FeatureMatching.use_gpu",
Expand Down
Loading
Loading