1use crate::{
48 CameraIntrinsics, ObjectRotation, RenderConfig, RenderHealth, RenderOutput, TargetingPolicy,
49};
50use bevy::prelude::{Transform, Vec3};
51use std::collections::VecDeque;
52use std::path::PathBuf;
53
54#[derive(Clone, Debug)]
56pub struct BatchRenderConfig {
57 pub max_batch_size: usize,
59 pub frame_timeout_ms: u32,
61 pub enable_depth_readback: bool,
63 pub enable_asset_caching: bool,
65 pub resource_cleanup_interval: u32,
67}
68
69impl Default for BatchRenderConfig {
70 fn default() -> Self {
71 Self {
72 max_batch_size: 256,
73 frame_timeout_ms: 500,
74 enable_depth_readback: true,
75 enable_asset_caching: true,
76 resource_cleanup_interval: 32,
77 }
78 }
79}
80
81#[derive(Clone, Debug)]
83pub struct BatchRenderRequest {
84 pub object_dir: PathBuf,
86 pub viewpoint: Transform,
88 pub object_rotation: ObjectRotation,
90 pub render_config: RenderConfig,
92 pub target_point: Vec3,
94 pub targeting_policy: TargetingPolicy,
96}
97
98#[derive(Clone, Debug, Copy, PartialEq, Eq)]
100pub enum RenderStatus {
101 Success,
103 PartialFailure,
105 Failed,
107}
108
109#[derive(Clone, Debug)]
111pub struct BatchRenderOutput {
112 pub request: BatchRenderRequest,
114 pub rgba: Vec<u8>,
116 pub depth: Vec<f64>,
118 pub width: u32,
120 pub height: u32,
122 pub intrinsics: CameraIntrinsics,
124 pub target_point: Vec3,
126 pub targeting_policy: TargetingPolicy,
128 pub health: RenderHealth,
130 pub status: RenderStatus,
132 pub error_message: Option<String>,
134}
135
136impl BatchRenderOutput {
137 pub fn to_rgb_image(&self) -> Vec<Vec<[u8; 3]>> {
139 let mut image = Vec::with_capacity(self.height as usize);
140 for y in 0..self.height {
141 let mut row = Vec::with_capacity(self.width as usize);
142 for x in 0..self.width {
143 let idx = ((y * self.width + x) * 4) as usize;
144 if idx + 2 < self.rgba.len() {
145 row.push([self.rgba[idx], self.rgba[idx + 1], self.rgba[idx + 2]]);
146 } else {
147 row.push([0, 0, 0]);
148 }
149 }
150 image.push(row);
151 }
152 image
153 }
154
155 pub fn to_depth_image(&self) -> Vec<Vec<f64>> {
157 let mut image = Vec::with_capacity(self.height as usize);
158 for y in 0..self.height {
159 let mut row = Vec::with_capacity(self.width as usize);
160 for x in 0..self.width {
161 let idx = (y * self.width + x) as usize;
162 if idx < self.depth.len() {
163 row.push(self.depth[idx]);
164 } else {
165 row.push(0.0);
166 }
167 }
168 image.push(row);
169 }
170 image
171 }
172
173 pub fn from_render_output(request: BatchRenderRequest, output: RenderOutput) -> Self {
175 let health = output.health_with_far_plane(request.render_config.far_plane as f64);
176 let target_point = request.target_point;
177 let targeting_policy = request.targeting_policy.clone();
178 Self {
179 request,
180 rgba: output.rgba,
181 depth: output.depth,
182 width: output.width,
183 height: output.height,
184 intrinsics: output.intrinsics,
185 target_point,
186 targeting_policy,
187 health,
188 status: RenderStatus::Success,
189 error_message: None,
190 }
191 }
192}
193
194#[derive(Debug, Clone)]
196pub enum BatchRenderError {
197 PartialFailure { successful: usize, failed: usize },
199 TotalFailure(String),
201 InvalidConfig(String),
203 QueueFull,
205 EmptyQueue,
207 DeviceLost { reason: String, message: String },
216}
217
218impl std::fmt::Display for BatchRenderError {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 match self {
221 BatchRenderError::PartialFailure { successful, failed } => {
222 write!(
223 f,
224 "Batch render partial failure: {} succeeded, {} failed",
225 successful, failed
226 )
227 }
228 BatchRenderError::TotalFailure(msg) => write!(f, "Batch render total failure: {}", msg),
229 BatchRenderError::InvalidConfig(msg) => write!(f, "Invalid batch config: {}", msg),
230 BatchRenderError::QueueFull => write!(f, "Batch queue is full"),
231 BatchRenderError::EmptyQueue => write!(f, "No renders queued"),
232 BatchRenderError::DeviceLost { reason, message } => {
233 write!(f, "wgpu device lost ({}): {}", reason, message)
234 }
235 }
236 }
237}
238
239impl std::error::Error for BatchRenderError {}
240
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243pub enum BatchState {
244 Idle,
246 LoadingAssets,
248 RenderingFrame,
250 ExtractingResults,
252 Cleanup,
254 Shutdown,
256}
257
258pub struct BatchRenderer {
260 pub pending_requests: VecDeque<BatchRenderRequest>,
262 pub completed_results: Vec<BatchRenderOutput>,
264 pub current_request: Option<BatchRenderRequest>,
266 pub current_output: Option<BatchRenderOutput>,
268 pub frame_count: u32,
270 pub state: BatchState,
272 pub config: BatchRenderConfig,
274 pub renders_processed: usize,
276}
277
278impl BatchRenderer {
279 pub fn new(config: BatchRenderConfig) -> Self {
281 Self {
282 pending_requests: VecDeque::new(),
283 completed_results: Vec::new(),
284 current_request: None,
285 current_output: None,
286 frame_count: 0,
287 state: BatchState::Idle,
288 config,
289 renders_processed: 0,
290 }
291 }
292
293 pub fn queue_request(&mut self, request: BatchRenderRequest) -> Result<(), BatchRenderError> {
295 if self.pending_requests.len() >= self.config.max_batch_size {
296 return Err(BatchRenderError::QueueFull);
297 }
298 self.pending_requests.push_back(request);
299 Ok(())
300 }
301
302 pub fn pending_count(&self) -> usize {
304 self.pending_requests.len()
305 }
306
307 pub fn completed_count(&self) -> usize {
309 self.completed_results.len()
310 }
311
312 pub fn take_completed(&mut self) -> Vec<BatchRenderOutput> {
314 std::mem::take(&mut self.completed_results)
315 }
316
317 pub fn is_finished(&self) -> bool {
319 self.pending_requests.is_empty() && self.current_request.is_none()
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_batch_config_defaults() {
329 let config = BatchRenderConfig::default();
330 assert_eq!(config.max_batch_size, 256);
331 assert_eq!(config.frame_timeout_ms, 500);
332 assert!(config.enable_depth_readback);
333 assert!(config.enable_asset_caching);
334 }
335
336 #[test]
337 fn test_batch_renderer_creation() {
338 let config = BatchRenderConfig::default();
339 let renderer = BatchRenderer::new(config);
340 assert_eq!(renderer.state, BatchState::Idle);
341 assert_eq!(renderer.pending_count(), 0);
342 assert_eq!(renderer.completed_count(), 0);
343 assert!(renderer.is_finished());
344 }
345
346 #[test]
347 fn test_queue_request() {
348 let mut renderer = BatchRenderer::new(BatchRenderConfig::default());
349 let request = BatchRenderRequest {
350 object_dir: "/tmp/test".into(),
351 viewpoint: Transform::default(),
352 object_rotation: ObjectRotation::identity(),
353 render_config: RenderConfig::tbp_default(),
354 target_point: Vec3::ZERO,
355 targeting_policy: TargetingPolicy::Origin,
356 };
357 assert!(renderer.queue_request(request).is_ok());
358 assert_eq!(renderer.pending_count(), 1);
359 }
360
361 #[test]
362 fn test_queue_full() {
363 let config = BatchRenderConfig {
364 max_batch_size: 1,
365 ..BatchRenderConfig::default()
366 };
367 let mut renderer = BatchRenderer::new(config);
368
369 let request = BatchRenderRequest {
370 object_dir: "/tmp/test".into(),
371 viewpoint: Transform::default(),
372 object_rotation: ObjectRotation::identity(),
373 render_config: RenderConfig::tbp_default(),
374 target_point: Vec3::ZERO,
375 targeting_policy: TargetingPolicy::Origin,
376 };
377
378 assert!(renderer.queue_request(request.clone()).is_ok());
379 assert!(matches!(
380 renderer.queue_request(request),
381 Err(BatchRenderError::QueueFull)
382 ));
383 }
384
385 #[test]
386 fn test_batch_render_output_rgb_conversion() {
387 let request = BatchRenderRequest {
388 object_dir: "/tmp/test".into(),
389 viewpoint: Transform::default(),
390 object_rotation: ObjectRotation::identity(),
391 render_config: RenderConfig::tbp_default(),
392 target_point: Vec3::ZERO,
393 targeting_policy: TargetingPolicy::Origin,
394 };
395
396 let mut rgba = vec![0u8; 2 * 2 * 4];
398 rgba[0] = 255;
400 rgba[1] = 0;
401 rgba[2] = 0;
402 rgba[3] = 255;
403
404 let output = BatchRenderOutput {
405 request,
406 rgba,
407 depth: vec![1.0; 4],
408 width: 2,
409 height: 2,
410 intrinsics: RenderConfig::tbp_default().intrinsics(),
411 target_point: Vec3::ZERO,
412 targeting_policy: TargetingPolicy::Origin,
413 health: RenderHealth {
414 center_pixel: Some([1, 1]),
415 center_depth: Some(1.0),
416 center_foreground: true,
417 foreground_pixel_count: 4,
418 foreground_coverage: 1.0,
419 center_5x5_foreground_count: 4,
420 nearest_foreground_pixel: Some([1, 1]),
421 nearest_foreground_depth: Some(1.0),
422 nearest_foreground_distance_px: Some(0.0),
423 },
424 status: RenderStatus::Success,
425 error_message: None,
426 };
427
428 let rgb = output.to_rgb_image();
429 assert_eq!(rgb.len(), 2); assert_eq!(rgb[0].len(), 2); assert_eq!(rgb[0][0], [255, 0, 0]); }
433
434 #[test]
435 fn test_batch_render_output_carries_request_target_metadata() {
436 let target_point = Vec3::new(0.25, -0.125, 0.5);
437 let request = BatchRenderRequest {
438 object_dir: "/tmp/test".into(),
439 viewpoint: Transform::default(),
440 object_rotation: ObjectRotation::identity(),
441 render_config: RenderConfig::tbp_default(),
442 target_point,
443 targeting_policy: TargetingPolicy::MeshCenter,
444 };
445 let output = RenderOutput {
446 rgba: vec![0u8; 4],
447 depth: vec![1.0],
448 width: 1,
449 height: 1,
450 intrinsics: RenderConfig::tbp_default().intrinsics(),
451 camera_transform: Transform::default(),
452 object_rotation: ObjectRotation::identity(),
453 target_point: Vec3::ZERO,
454 targeting_policy: TargetingPolicy::Origin,
455 };
456
457 let batch_output = BatchRenderOutput::from_render_output(request, output);
458
459 assert_eq!(batch_output.target_point, target_point);
460 assert_eq!(batch_output.targeting_policy, TargetingPolicy::MeshCenter);
461 assert_eq!(batch_output.request.target_point, target_point);
462 assert_eq!(
463 batch_output.request.targeting_policy,
464 TargetingPolicy::MeshCenter
465 );
466 }
467}