BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / render_job.rs

render_job.rs

render_job.rs 9.5 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}
100
101impl RenderUiState {
102 pub fn idle() -> Self {
103 Self {
104 progress: Progress {
105 completed: 0,
106 total: 0,
107 label: String::new(),
108 },
109 preview: None,
110 preview_label: String::new(),
111 preview_revision: 0,
112 saved: None,
113 error: None,
114 running: false,
115 }
116 }
117
118 pub fn apply(&mut self, event: RenderEvent) {
119 match event {
120 RenderEvent::Step {
121 index,
122 total,
123 label,
124 } => {
125 self.progress = Progress {
126 completed: index,
127 total,
128 label,
129 };
130 self.running = true;
131 }
132 RenderEvent::Pass {
133 index,
134 total,
135 label,
136 image,
137 } => {
138 self.progress = Progress {
139 completed: index + 1,
140 total,
141 label,
142 };
143 if let Some(image) = image {
144 self.preview_label = self.progress.label.clone();
145 self.preview_revision = self.preview_revision.wrapping_add(1);
146 self.preview = Some(image);
147 }
148 self.running = true;
149 }
150 RenderEvent::Cancelled => {
151 self.running = false;
152 self.progress.label = "Geannuleerd; gedeeltelijke uitvoer behouden".into();
153 }
154 RenderEvent::Failed(message) => {
155 self.error = Some(message);
156 self.running = false;
157 }
158 RenderEvent::Finished { saved } => {
159 self.saved = saved;
160 self.running = false;
161 if self.progress.total > 0 {
162 self.progress.completed = self.progress.total;
163 }
164 }
165 }
166 }
167}
168
169pub fn export_views() -> Vec<ViewAngle> {
170 let mut views = Vec::with_capacity(PAPERDOLL_PITCHES_DEG.len() * paperdoll_yaws_deg().len());
171 for pitch in PAPERDOLL_PITCHES_DEG {
172 for yaw in paperdoll_yaws_deg() {
173 views.push(ViewAngle {
174 pitch_deg: pitch,
175 yaw_deg: yaw,
176 });
177 }
178 }
179 views
180}
181
182pub fn pass_sequence(views: &[ViewAngle]) -> Vec<PassStep> {
183 let mut steps = Vec::with_capacity(views.len() * PASS_ORDER.len());
184 for view in views {
185 for kind in PASS_ORDER {
186 steps.push(PassStep {
187 pitch_deg: view.pitch_deg,
188 yaw_deg: view.yaw_deg,
189 kind,
190 });
191 }
192 }
193 steps
194}
195
196/// Nearest-neighbor stretch. Every destination pixel is filled, so the
197/// preview box has no empty border.
198pub fn nearest_stretch(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
199 let mut out = vec![0u8; (dw as usize) * (dh as usize) * 4];
200 if sw == 0 || sh == 0 || dw == 0 || dh == 0 {
201 return out;
202 }
203 for y in 0..dh {
204 let sy = ((y as u64 * sh as u64) / dh as u64) as u32;
205 let sy = sy.min(sh - 1);
206 for x in 0..dw {
207 let sx = ((x as u64 * sw as u64) / dw as u64) as u32;
208 let sx = sx.min(sw - 1);
209 let from = ((sy * sw + sx) * 4) as usize;
210 let to = ((y * dw + x) * 4) as usize;
211 out[to..to + 4].copy_from_slice(&src[from..from + 4]);
212 }
213 }
214 out
215}
216
217#[derive(Debug, Clone, Copy, PartialEq)]
218pub struct PreviewFit {
219 pub size: [f32; 2],
220 pub offset: [f32; 2],
221}
222
223pub fn fit_preview(image: [u32; 2], bounds: [f32; 2]) -> PreviewFit {
224 if image.contains(&0) || bounds.iter().any(|v| !v.is_finite() || *v <= 0.0) {
225 return PreviewFit {
226 size: [0.; 2],
227 offset: [0.; 2],
228 };
229 }
230 let scale = (bounds[0] / image[0] as f32).min(bounds[1] / image[1] as f32);
231 let size = [image[0] as f32 * scale, image[1] as f32 * scale];
232 PreviewFit {
233 size,
234 offset: [(bounds[0] - size[0]) * 0.5, (bounds[1] - size[1]) * 0.5],
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn full_view_order_is_pitch_then_yaw_then_passes() {
244 let views = export_views();
245 assert_eq!(views.len(), 60);
246 assert_eq!(views[0].pitch_deg, -60.0);
247 assert_eq!(views[0].yaw_deg, 0.0);
248 assert_eq!(views[11].yaw_deg, 330.0);
249 assert_eq!(views[12].pitch_deg, -30.0);
250 assert_eq!(views[24].pitch_deg, 0.0);
251 assert_eq!(views[59].pitch_deg, 60.0);
252 let steps = pass_sequence(&views);
253 assert_eq!(steps.len(), 60 * 6);
254 let first: Vec<_> = steps.iter().take(6).map(|step| step.kind).collect();
255 assert_eq!(first, PASS_ORDER.to_vec());
256 assert_eq!(steps[6].yaw_deg, 30.0);
257 assert_eq!(steps[6].kind, PassKind::HighresImage);
258 }
259
260 #[test]
261 fn preview_uniform_scale_centers_and_letterboxes_all_aspects() {
262 let square = fit_preview([128, 128], [480., 640.]);
263 assert_eq!(square.size, [480., 480.]);
264 assert_eq!(square.offset, [0., 80.]);
265 let landscape = fit_preview([640, 320], [300., 300.]);
266 assert_eq!(landscape.size, [300., 150.]);
267 assert_eq!(landscape.offset, [0., 75.]);
268 let portrait = fit_preview([64, 128], [400., 200.]);
269 assert_eq!(portrait.size, [100., 200.]);
270 assert_eq!(portrait.offset, [150., 0.]);
271 assert_eq!(fit_preview([0, 128], [400., 200.]).size, [0., 0.]);
272 assert_eq!(fit_preview([128, 128], [f32::NAN, 200.]).size, [0., 0.]);
273 }
274
275 #[test]
276 fn nearest_non_square_uses_source_height_for_rows() {
277 let src = [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
278 let out = nearest_stretch(&src, 1, 3, 2, 6);
279 for y in 0..6 {
280 assert_eq!(&out[y * 8..y * 8 + 4], &src[(y / 2) * 4..(y / 2) * 4 + 4]);
281 }
282 }
283
284 #[test]
285 fn nearest_stretch_fills_the_box() {
286 let src = [
287 255, 0, 0, 255, 0, 0, 255, 255, // 2x1
288 ];
289 let out = nearest_stretch(&src, 2, 1, 4, 3);
290 assert_eq!(out.len(), 4 * 3 * 4);
291 for y in 0..3 {
292 for x in 0..4 {
293 let pixel = &out[(y * 4 + x) * 4..(y * 4 + x) * 4 + 4];
294 if x < 2 {
295 assert_eq!(pixel, &[255, 0, 0, 255]);
296 } else {
297 assert_eq!(pixel, &[0, 0, 255, 255]);
298 }
299 }
300 }
301 }
302
303 #[test]
304 fn pass_progress_cancellation_and_last_image_are_preserved() {
305 let mut ui = RenderUiState::idle();
306 ui.apply(RenderEvent::Pass {
307 index: 1,
308 total: 6,
309 label: "detail".into(),
310 image: Some(ImageBuf {
311 width: 1,
312 height: 1,
313 rgba: vec![1, 2, 3, 255],
314 }),
315 });
316 ui.apply(RenderEvent::Step {
317 index: 2,
318 total: 6,
319 label: "target".into(),
320 });
321 assert_eq!(ui.preview.as_ref().unwrap().rgba, vec![1, 2, 3, 255]);
322 assert_eq!(ui.preview_revision, 1);
323 assert_eq!(ui.preview_label, "detail");
324 assert_eq!(ui.progress.label, "target");
325 ui.apply(RenderEvent::Cancelled);
326 assert!(!ui.running);
327 assert_eq!(ui.progress.completed, 2);
328 assert_eq!(ui.preview_revision, 1);
329 assert_eq!(ui.preview.as_ref().unwrap().rgba, vec![1, 2, 3, 255]);
330 }
331}