BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / render_job.rs

render_job.rs

render_job.rs 10.4 KiB Download Raw
1//! Actual production pass order and GUI state. The bounded worker/mailbox and
2//! GPU service are implemented in `production`; previews retain native size.
3
4use crate::preview::{paperdoll_yaws_deg, PAPERDOLL_PITCHES_DEG};
5pub use crate::production::{spawn, spawn_resume, RenderJob, RenderThread};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum PassKind {
9 HighresImage,
10 DetailMap,
11 TargetResolution,
12 Depth,
13 Normal,
14 Specular,
15}
16
17impl PassKind {
18 pub fn label(self) -> &'static str {
19 match self {
20 Self::HighresImage => "highres",
21 Self::DetailMap => "detail",
22 Self::TargetResolution => "doelresolutie",
23 Self::Depth => "depth",
24 Self::Normal => "normal",
25 Self::Specular => "specular",
26 }
27 }
28
29 pub fn produces_image(self) -> bool {
30 true
31 }
32}
33
34pub const PASS_ORDER: [PassKind; 6] = [
35 PassKind::HighresImage,
36 PassKind::DetailMap,
37 PassKind::TargetResolution,
38 PassKind::Depth,
39 PassKind::Normal,
40 PassKind::Specular,
41];
42
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct ViewAngle {
45 pub pitch_deg: f32,
46 pub yaw_deg: f32,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct PassStep {
51 pub pitch_deg: f32,
52 pub yaw_deg: f32,
53 pub kind: PassKind,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ImageBuf {
58 pub width: u32,
59 pub height: u32,
60 pub rgba: Vec<u8>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Progress {
65 pub completed: u32,
66 pub total: u32,
67 pub label: String,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum RenderEvent {
72 Step {
73 index: u32,
74 total: u32,
75 label: String,
76 },
77 Pass {
78 index: u32,
79 total: u32,
80 label: String,
81 image: Option<ImageBuf>,
82 },
83 Failed(String),
84 Cancelled,
85 Finished {
86 saved: Option<String>,
87 },
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct RenderUiState {
92 pub progress: Progress,
93 pub preview: Option<ImageBuf>,
94 pub preview_label: String,
95 pub preview_revision: u64,
96 pub saved: Option<String>,
97 pub error: Option<String>,
98 pub running: bool,
99 /// Every warning step of this render (e.g. clamped depth per view),
100 /// kept so a later pass label cannot hide it. Capped at `MAX_WARNINGS`.
101 pub warnings: Vec<String>,
102}
103
104pub const MAX_WARNINGS: usize = 100_000;
105
106impl RenderUiState {
107 pub fn idle() -> Self {
108 Self {
109 progress: Progress {
110 completed: 0,
111 total: 0,
112 label: String::new(),
113 },
114 preview: None,
115 preview_label: String::new(),
116 preview_revision: 0,
117 saved: None,
118 error: None,
119 running: false,
120 warnings: Vec::new(),
121 }
122 }
123
124 pub fn apply(&mut self, event: RenderEvent) {
125 match event {
126 RenderEvent::Step {
127 index,
128 total,
129 label,
130 } => {
131 if label.starts_with("Waarschuwing") && self.warnings.len() < MAX_WARNINGS {
132 self.warnings.push(label.clone());
133 }
134 self.progress = Progress {
135 completed: index,
136 total,
137 label,
138 };
139 self.running = true;
140 }
141 RenderEvent::Pass {
142 index,
143 total,
144 label,
145 image,
146 } => {
147 self.progress = Progress {
148 completed: index + 1,
149 total,
150 label,
151 };
152 if let Some(image) = image {
153 self.preview_label = self.progress.label.clone();
154 self.preview_revision = self.preview_revision.wrapping_add(1);
155 self.preview = Some(image);
156 }
157 self.running = true;
158 }
159 RenderEvent::Cancelled => {
160 self.running = false;
161 self.progress.label = "Geannuleerd; gedeeltelijke uitvoer behouden".into();
162 }
163 RenderEvent::Failed(message) => {
164 self.error = Some(message);
165 self.running = false;
166 }
167 RenderEvent::Finished { saved } => {
168 self.saved = saved;
169 self.running = false;
170 if self.progress.total > 0 {
171 self.progress.completed = self.progress.total;
172 }
173 }
174 }
175 }
176}
177
178pub fn export_views() -> Vec<ViewAngle> {
179 let mut views = Vec::with_capacity(PAPERDOLL_PITCHES_DEG.len() * paperdoll_yaws_deg().len());
180 for pitch in PAPERDOLL_PITCHES_DEG {
181 for yaw in paperdoll_yaws_deg() {
182 views.push(ViewAngle {
183 pitch_deg: pitch,
184 yaw_deg: yaw,
185 });
186 }
187 }
188 views
189}
190
191pub fn pass_sequence(views: &[ViewAngle]) -> Vec<PassStep> {
192 let mut steps = Vec::with_capacity(views.len() * PASS_ORDER.len());
193 for view in views {
194 for kind in PASS_ORDER {
195 steps.push(PassStep {
196 pitch_deg: view.pitch_deg,
197 yaw_deg: view.yaw_deg,
198 kind,
199 });
200 }
201 }
202 steps
203}
204
205/// Nearest-neighbor stretch. Every destination pixel is filled, so the
206/// preview box has no empty border.
207pub fn nearest_stretch(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
208 let mut out = vec![0u8; (dw as usize) * (dh as usize) * 4];
209 if sw == 0 || sh == 0 || dw == 0 || dh == 0 {
210 return out;
211 }
212 for y in 0..dh {
213 let sy = ((y as u64 * sh as u64) / dh as u64) as u32;
214 let sy = sy.min(sh - 1);
215 for x in 0..dw {
216 let sx = ((x as u64 * sw as u64) / dw as u64) as u32;
217 let sx = sx.min(sw - 1);
218 let from = ((sy * sw + sx) * 4) as usize;
219 let to = ((y * dw + x) * 4) as usize;
220 out[to..to + 4].copy_from_slice(&src[from..from + 4]);
221 }
222 }
223 out
224}
225
226#[derive(Debug, Clone, Copy, PartialEq)]
227pub struct PreviewFit {
228 pub size: [f32; 2],
229 pub offset: [f32; 2],
230}
231
232pub fn fit_preview(image: [u32; 2], bounds: [f32; 2]) -> PreviewFit {
233 if image.contains(&0) || bounds.iter().any(|v| !v.is_finite() || *v <= 0.0) {
234 return PreviewFit {
235 size: [0.; 2],
236 offset: [0.; 2],
237 };
238 }
239 let scale = (bounds[0] / image[0] as f32).min(bounds[1] / image[1] as f32);
240 let size = [image[0] as f32 * scale, image[1] as f32 * scale];
241 PreviewFit {
242 size,
243 offset: [(bounds[0] - size[0]) * 0.5, (bounds[1] - size[1]) * 0.5],
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn full_view_order_is_pitch_then_yaw_then_passes() {
253 let views = export_views();
254 assert_eq!(views.len(), 60);
255 assert_eq!(views[0].pitch_deg, -60.0);
256 assert_eq!(views[0].yaw_deg, 0.0);
257 assert_eq!(views[11].yaw_deg, 330.0);
258 assert_eq!(views[12].pitch_deg, -30.0);
259 assert_eq!(views[24].pitch_deg, 0.0);
260 assert_eq!(views[59].pitch_deg, 60.0);
261 let steps = pass_sequence(&views);
262 assert_eq!(steps.len(), 60 * 6);
263 let first: Vec<_> = steps.iter().take(6).map(|step| step.kind).collect();
264 assert_eq!(first, PASS_ORDER.to_vec());
265 assert_eq!(steps[6].yaw_deg, 30.0);
266 assert_eq!(steps[6].kind, PassKind::HighresImage);
267 }
268
269 #[test]
270 fn preview_uniform_scale_centers_and_letterboxes_all_aspects() {
271 let square = fit_preview([128, 128], [480., 640.]);
272 assert_eq!(square.size, [480., 480.]);
273 assert_eq!(square.offset, [0., 80.]);
274 let landscape = fit_preview([640, 320], [300., 300.]);
275 assert_eq!(landscape.size, [300., 150.]);
276 assert_eq!(landscape.offset, [0., 75.]);
277 let portrait = fit_preview([64, 128], [400., 200.]);
278 assert_eq!(portrait.size, [100., 200.]);
279 assert_eq!(portrait.offset, [150., 0.]);
280 assert_eq!(fit_preview([0, 128], [400., 200.]).size, [0., 0.]);
281 assert_eq!(fit_preview([128, 128], [f32::NAN, 200.]).size, [0., 0.]);
282 }
283
284 #[test]
285 fn nearest_non_square_uses_source_height_for_rows() {
286 let src = [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
287 let out = nearest_stretch(&src, 1, 3, 2, 6);
288 for y in 0..6 {
289 assert_eq!(&out[y * 8..y * 8 + 4], &src[(y / 2) * 4..(y / 2) * 4 + 4]);
290 }
291 }
292
293 #[test]
294 fn nearest_stretch_fills_the_box() {
295 let src = [
296 255, 0, 0, 255, 0, 0, 255, 255, // 2x1
297 ];
298 let out = nearest_stretch(&src, 2, 1, 4, 3);
299 assert_eq!(out.len(), 4 * 3 * 4);
300 for y in 0..3 {
301 for x in 0..4 {
302 let pixel = &out[(y * 4 + x) * 4..(y * 4 + x) * 4 + 4];
303 if x < 2 {
304 assert_eq!(pixel, &[255, 0, 0, 255]);
305 } else {
306 assert_eq!(pixel, &[0, 0, 255, 255]);
307 }
308 }
309 }
310 }
311
312 #[test]
313 fn pass_progress_cancellation_and_last_image_are_preserved() {
314 let mut ui = RenderUiState::idle();
315 ui.apply(RenderEvent::Pass {
316 index: 1,
317 total: 6,
318 label: "detail".into(),
319 image: Some(ImageBuf {
320 width: 1,
321 height: 1,
322 rgba: vec![1, 2, 3, 255],
323 }),
324 });
325 ui.apply(RenderEvent::Step {
326 index: 2,
327 total: 6,
328 label: "target".into(),
329 });
330 assert_eq!(ui.preview.as_ref().unwrap().rgba, vec![1, 2, 3, 255]);
331 assert_eq!(ui.preview_revision, 1);
332 assert_eq!(ui.preview_label, "detail");
333 assert_eq!(ui.progress.label, "target");
334 ui.apply(RenderEvent::Cancelled);
335 assert!(!ui.running);
336 assert_eq!(ui.progress.completed, 2);
337 assert_eq!(ui.preview_revision, 1);
338 assert_eq!(ui.preview.as_ref().unwrap().rgba, vec![1, 2, 3, 255]);
339 }
340
341 #[test]
342 fn warning_steps_are_kept_after_later_passes() {
343 let mut ui = RenderUiState::idle();
344 for label in [
345 "Waarschuwing: x · 3 highres-pixels buiten dieptebereik (geklemd)",
346 "target",
347 ] {
348 ui.apply(RenderEvent::Step {
349 index: 1,
350 total: 2,
351 label: label.into(),
352 });
353 }
354 assert_eq!(ui.progress.label, "target");
355 assert_eq!(ui.warnings.len(), 1);
356 }
357}