| 1 | //! Real native GPU proof for production resume (no input automation, no delays). |
| 2 | //! Usage: |
| 3 | //! resume_fixture setup ROOT MALE.glb FEMALE.glb WIDTH HEIGHT FPS |
| 4 | //! resume_fixture render ROOT male|female [--cancel-after=N] [--abort-after=N] |
| 5 | //! resume_fixture verify ROOT (ExportRun::open on every run) |
| 6 | //! resume_fixture resume ROOT male|female [--cancel-after=N] [--screenshot=PATH] |
| 7 | //! `--abort-after` kills the process with `abort()` (a real crash: no terminal |
| 8 | //! marker). Each GUI invocation writes ROOT/<mode>-result.json. |
| 9 | use eframe::App; |
| 10 | use paperdoll_renderer::{ |
| 11 | editor::EditorApp, |
| 12 | session::{adopt_file, Session, Sex}, |
| 13 | }; |
| 14 | use std::path::PathBuf; |
| 15 | |
| 16 | struct Fixture { |
| 17 | editor: EditorApp, |
| 18 | root: PathBuf, |
| 19 | mode: String, |
| 20 | cancel_after: Option<u32>, |
| 21 | abort_after: Option<u32>, |
| 22 | screenshot: Option<PathBuf>, |
| 23 | phase: u8, |
| 24 | cancel_sent: bool, |
| 25 | frames: u64, |
| 26 | last: u32, |
| 27 | done: bool, |
| 28 | candidates: serde_json::Value, |
| 29 | } |
| 30 | |
| 31 | impl App for Fixture { |
| 32 | fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) { |
| 33 | self.frames += 1; |
| 34 | self.editor.update(ctx, frame); |
| 35 | match self.phase { |
| 36 | 0 => { |
| 37 | if self.mode == "resume" { |
| 38 | self.editor.scan_resumable_runs(); |
| 39 | self.candidates = serde_json::Value::Array( |
| 40 | self.editor |
| 41 | .resumable_runs() |
| 42 | .iter() |
| 43 | .map(|c| { |
| 44 | serde_json::json!({"name":c.name,"state":c.state,"journaled":c.journaled,"planned":c.planned,"blocked":c.blocked}) |
| 45 | }) |
| 46 | .collect(), |
| 47 | ); |
| 48 | println!("CANDIDATES {}", self.candidates); |
| 49 | self.phase = 1; |
| 50 | } else { |
| 51 | self.editor.render_all_angles(); |
| 52 | self.phase = 3; |
| 53 | } |
| 54 | ctx.request_repaint(); |
| 55 | return; |
| 56 | } |
| 57 | 1 => { |
| 58 | if self.screenshot.is_some() { |
| 59 | ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Screenshot); |
| 60 | self.phase = 2; |
| 61 | ctx.request_repaint(); |
| 62 | return; |
| 63 | } |
| 64 | self.start_resume(ctx); |
| 65 | return; |
| 66 | } |
| 67 | 2 => { |
| 68 | let shot = ctx.input(|i| { |
| 69 | i.raw.events.iter().find_map(|e| match e { |
| 70 | eframe::egui::Event::Screenshot { image, .. } => Some(image.clone()), |
| 71 | _ => None, |
| 72 | }) |
| 73 | }); |
| 74 | if let (Some(image), Some(path)) = (shot, &self.screenshot) { |
| 75 | let [w, h] = image.size; |
| 76 | let rgba: Vec<u8> = image.pixels.iter().flat_map(|p| p.to_array()).collect(); |
| 77 | let file = std::fs::File::create(path).unwrap(); |
| 78 | let mut enc = |
| 79 | png::Encoder::new(std::io::BufWriter::new(file), w as u32, h as u32); |
| 80 | enc.set_color(png::ColorType::Rgba); |
| 81 | enc.set_depth(png::BitDepth::Eight); |
| 82 | enc.write_header().unwrap().write_image_data(&rgba).unwrap(); |
| 83 | println!("SCREENSHOT {}", path.display()); |
| 84 | self.start_resume(ctx); |
| 85 | } |
| 86 | ctx.request_repaint(); |
| 87 | return; |
| 88 | } |
| 89 | _ => {} |
| 90 | } |
| 91 | let (completed, total, label) = self.editor.render_progress(); |
| 92 | if completed != self.last { |
| 93 | println!("PASS {completed}/{total}: {label}"); |
| 94 | self.last = completed; |
| 95 | } |
| 96 | if let Some(n) = self.abort_after { |
| 97 | if completed >= n { |
| 98 | println!("ABORT at {completed}/{total}"); |
| 99 | std::process::abort(); |
| 100 | } |
| 101 | } |
| 102 | if let Some(n) = self.cancel_after { |
| 103 | if completed >= n && !self.cancel_sent && self.editor.render_is_running() { |
| 104 | self.cancel_sent = true; |
| 105 | println!("CANCEL at {completed}/{total}"); |
| 106 | self.editor.request_render_cancel(); |
| 107 | } |
| 108 | } |
| 109 | if !self.editor.render_is_running() && !self.done { |
| 110 | let error = self.editor.render_error().map(str::to_string); |
| 111 | self.finish(ctx, error); |
| 112 | } |
| 113 | ctx.request_repaint(); |
| 114 | } |
| 115 | fn on_exit(&mut self, gl: Option<&glow::Context>) { |
| 116 | self.editor.on_exit(gl); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | impl Fixture { |
| 121 | fn start_resume(&mut self, ctx: &eframe::egui::Context) { |
| 122 | self.phase = 3; |
| 123 | let Some(dir) = self |
| 124 | .editor |
| 125 | .resumable_runs() |
| 126 | .iter() |
| 127 | .find(|c| c.blocked.is_none()) |
| 128 | .map(|c| c.dir.clone()) |
| 129 | else { |
| 130 | self.finish(ctx, Some("geen hervatbare run".into())); |
| 131 | return; |
| 132 | }; |
| 133 | self.editor.resume_run(dir); |
| 134 | } |
| 135 | fn finish(&mut self, ctx: &eframe::egui::Context, error: Option<String>) { |
| 136 | self.done = true; |
| 137 | let (completed, total, label) = self.editor.render_progress(); |
| 138 | let value = serde_json::json!({ |
| 139 | "mode": self.mode, |
| 140 | "completed": completed, |
| 141 | "total": total, |
| 142 | "last_label": label, |
| 143 | "cancel_sent": self.cancel_sent, |
| 144 | "error": error, |
| 145 | "candidates_before": self.candidates, |
| 146 | "gui_updates": self.frames, |
| 147 | }); |
| 148 | std::fs::write( |
| 149 | self.root.join(format!("{}-result.json", self.mode)), |
| 150 | serde_json::to_vec_pretty(&value).unwrap(), |
| 151 | ) |
| 152 | .unwrap(); |
| 153 | println!("RESULT {value}"); |
| 154 | ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 159 | let a = std::env::args().skip(1).collect::<Vec<_>>(); |
| 160 | let mode = a.first().ok_or("mode")?.clone(); |
| 161 | let root = PathBuf::from(a.get(1).ok_or("ROOT")?); |
| 162 | if mode == "verify" { |
| 163 | // Open every run directory through the strict schema-3 export_set reader. |
| 164 | let mut dirs = std::fs::read_dir(root.join("renders"))? |
| 165 | .map(|e| e.map(|e| e.path())) |
| 166 | .collect::<Result<Vec<_>, _>>()?; |
| 167 | dirs.sort(); |
| 168 | for dir in dirs { |
| 169 | match paperdoll_renderer::export_set::ExportRun::open(&dir) { |
| 170 | Ok(run) => println!( |
| 171 | "OPEN-OK {} png_files={}", |
| 172 | dir.display(), |
| 173 | run.complete.png_files |
| 174 | ), |
| 175 | Err(e) => println!("OPEN-REJECTED {}: {e}", dir.display()), |
| 176 | } |
| 177 | } |
| 178 | return Ok(()); |
| 179 | } |
| 180 | if mode == "setup" { |
| 181 | let mut s = Session::new(); |
| 182 | s.install_new_project(&root)?; |
| 183 | for (sex, index) in [(Sex::Male, 2), (Sex::Female, 3)] { |
| 184 | let path = adopt_file(&root, &PathBuf::from(&a[index]), "assets/body")?; |
| 185 | s.set_body_path(sex, path); |
| 186 | } |
| 187 | s.set_render(a[4].parse()?, a[5].parse()?, a[6].parse()?); |
| 188 | s.save()?; |
| 189 | return Ok(()); |
| 190 | } |
| 191 | let sex = match a.get(2).map(String::as_str) { |
| 192 | Some("male") => Sex::Male, |
| 193 | Some("female") => Sex::Female, |
| 194 | _ => return Err("male|female".into()), |
| 195 | }; |
| 196 | let flag = |name: &str| { |
| 197 | a.iter() |
| 198 | .find_map(|v| v.strip_prefix(name)) |
| 199 | .map(|v| v.to_string()) |
| 200 | }; |
| 201 | let cancel_after = flag("--cancel-after=").map(|v| v.parse()).transpose()?; |
| 202 | let abort_after = flag("--abort-after=").map(|v| v.parse()).transpose()?; |
| 203 | let screenshot = flag("--screenshot=").map(PathBuf::from); |
| 204 | let root2 = root.clone(); |
| 205 | let mode2 = mode.clone(); |
| 206 | eframe::run_native( |
| 207 | "Paperdoll Resume Fixture", |
| 208 | eframe::NativeOptions { |
| 209 | viewport: eframe::egui::ViewportBuilder::default().with_inner_size([1150., 820.]), |
| 210 | ..paperdoll_renderer::preview_gl::native_options() |
| 211 | }, |
| 212 | Box::new(move |_| { |
| 213 | let mut editor = EditorApp::default(); |
| 214 | editor.open_path(root2.join("character.ppd")); |
| 215 | editor.open_body_configuration(sex); |
| 216 | Ok(Box::new(Fixture { |
| 217 | editor, |
| 218 | root: root2, |
| 219 | mode: mode2, |
| 220 | cancel_after, |
| 221 | abort_after, |
| 222 | screenshot, |
| 223 | phase: 0, |
| 224 | cancel_sent: false, |
| 225 | frames: 0, |
| 226 | last: 0, |
| 227 | done: false, |
| 228 | candidates: serde_json::Value::Null, |
| 229 | })) |
| 230 | }), |
| 231 | )?; |
| 232 | let report: serde_json::Value = |
| 233 | serde_json::from_slice(&std::fs::read(root.join(format!("{mode}-result.json")))?)?; |
| 234 | println!("FINAL {report}"); |
| 235 | Ok(()) |
| 236 | } |