//! Real native GPU proof for production resume (no input automation, no delays). //! Usage: //! resume_fixture setup ROOT MALE.glb FEMALE.glb WIDTH HEIGHT FPS //! resume_fixture render ROOT male|female [--cancel-after=N] [--abort-after=N] //! resume_fixture verify ROOT (ExportRun::open on every run) //! resume_fixture resume ROOT male|female [--cancel-after=N] [--screenshot=PATH] //! `--abort-after` kills the process with `abort()` (a real crash: no terminal //! marker). Each GUI invocation writes ROOT/-result.json. use eframe::App; use paperdoll_renderer::{ editor::EditorApp, session::{adopt_file, Session, Sex}, }; use std::path::PathBuf; struct Fixture { editor: EditorApp, root: PathBuf, mode: String, cancel_after: Option, abort_after: Option, screenshot: Option, phase: u8, cancel_sent: bool, frames: u64, last: u32, done: bool, candidates: serde_json::Value, } impl App for Fixture { fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) { self.frames += 1; self.editor.update(ctx, frame); match self.phase { 0 => { if self.mode == "resume" { self.editor.scan_resumable_runs(); self.candidates = serde_json::Value::Array( self.editor .resumable_runs() .iter() .map(|c| { serde_json::json!({"name":c.name,"state":c.state,"journaled":c.journaled,"planned":c.planned,"blocked":c.blocked}) }) .collect(), ); println!("CANDIDATES {}", self.candidates); self.phase = 1; } else { self.editor.render_all_angles(); self.phase = 3; } ctx.request_repaint(); return; } 1 => { if self.screenshot.is_some() { ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Screenshot); self.phase = 2; ctx.request_repaint(); return; } self.start_resume(ctx); return; } 2 => { let shot = ctx.input(|i| { i.raw.events.iter().find_map(|e| match e { eframe::egui::Event::Screenshot { image, .. } => Some(image.clone()), _ => None, }) }); if let (Some(image), Some(path)) = (shot, &self.screenshot) { let [w, h] = image.size; let rgba: Vec = image.pixels.iter().flat_map(|p| p.to_array()).collect(); let file = std::fs::File::create(path).unwrap(); let mut enc = png::Encoder::new(std::io::BufWriter::new(file), w as u32, h as u32); enc.set_color(png::ColorType::Rgba); enc.set_depth(png::BitDepth::Eight); enc.write_header().unwrap().write_image_data(&rgba).unwrap(); println!("SCREENSHOT {}", path.display()); self.start_resume(ctx); } ctx.request_repaint(); return; } _ => {} } let (completed, total, label) = self.editor.render_progress(); if completed != self.last { println!("PASS {completed}/{total}: {label}"); self.last = completed; } if let Some(n) = self.abort_after { if completed >= n { println!("ABORT at {completed}/{total}"); std::process::abort(); } } if let Some(n) = self.cancel_after { if completed >= n && !self.cancel_sent && self.editor.render_is_running() { self.cancel_sent = true; println!("CANCEL at {completed}/{total}"); self.editor.request_render_cancel(); } } if !self.editor.render_is_running() && !self.done { let error = self.editor.render_error().map(str::to_string); self.finish(ctx, error); } ctx.request_repaint(); } fn on_exit(&mut self, gl: Option<&glow::Context>) { self.editor.on_exit(gl); } } impl Fixture { fn start_resume(&mut self, ctx: &eframe::egui::Context) { self.phase = 3; let Some(dir) = self .editor .resumable_runs() .iter() .find(|c| c.blocked.is_none()) .map(|c| c.dir.clone()) else { self.finish(ctx, Some("geen hervatbare run".into())); return; }; self.editor.resume_run(dir); } fn finish(&mut self, ctx: &eframe::egui::Context, error: Option) { self.done = true; let (completed, total, label) = self.editor.render_progress(); let value = serde_json::json!({ "mode": self.mode, "completed": completed, "total": total, "last_label": label, "cancel_sent": self.cancel_sent, "error": error, "candidates_before": self.candidates, "gui_updates": self.frames, }); std::fs::write( self.root.join(format!("{}-result.json", self.mode)), serde_json::to_vec_pretty(&value).unwrap(), ) .unwrap(); println!("RESULT {value}"); ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); } } fn main() -> Result<(), Box> { let a = std::env::args().skip(1).collect::>(); let mode = a.first().ok_or("mode")?.clone(); let root = PathBuf::from(a.get(1).ok_or("ROOT")?); if mode == "verify" { // Open every run directory through export_set and the .ppda atlas reader. let mut dirs = std::fs::read_dir(root.join("renders"))? .map(|e| e.map(|e| e.path())) .collect::, _>>()?; dirs.sort(); for dir in dirs { match paperdoll_renderer::export_set::ExportRun::open(&dir) { Ok(run) => { let atlas = paperdoll_renderer::atlas::cross_check_run(&dir) .map(|a| a["cells_equal_to_loose_pngs"].to_string()) .unwrap_or_else(|e| format!("FOUT {e}")); println!( "OPEN-OK {} png_files={} atlas_cells={atlas}", dir.display(), run.complete.png_files ) } Err(e) => println!("OPEN-REJECTED {}: {e}", dir.display()), } } return Ok(()); } if mode == "setup" { let mut s = Session::new(); s.install_new_project(&root)?; for (sex, index) in [(Sex::Male, 2), (Sex::Female, 3)] { let path = adopt_file(&root, &PathBuf::from(&a[index]), "assets/body")?; s.set_body_path(sex, path); } s.set_render(a[4].parse()?, a[5].parse()?, a[6].parse()?); s.save()?; return Ok(()); } let sex = match a.get(2).map(String::as_str) { Some("male") => Sex::Male, Some("female") => Sex::Female, _ => return Err("male|female".into()), }; let flag = |name: &str| { a.iter() .find_map(|v| v.strip_prefix(name)) .map(|v| v.to_string()) }; let cancel_after = flag("--cancel-after=").map(|v| v.parse()).transpose()?; let abort_after = flag("--abort-after=").map(|v| v.parse()).transpose()?; let screenshot = flag("--screenshot=").map(PathBuf::from); let root2 = root.clone(); let mode2 = mode.clone(); eframe::run_native( "Paperdoll Resume Fixture", eframe::NativeOptions { viewport: eframe::egui::ViewportBuilder::default().with_inner_size([1150., 820.]), ..paperdoll_renderer::preview_gl::native_options() }, Box::new(move |_| { let mut editor = EditorApp::default(); editor.open_path(root2.join("character.ppd")); editor.open_body_configuration(sex); Ok(Box::new(Fixture { editor, root: root2, mode: mode2, cancel_after, abort_after, screenshot, phase: 0, cancel_sent: false, frames: 0, last: 0, done: false, candidates: serde_json::Value::Null, })) }), )?; let report: serde_json::Value = serde_json::from_slice(&std::fs::read(root.join(format!("{mode}-result.json")))?)?; println!("FINAL {report}"); Ok(()) }