1use crate::{
48 semantic_3d_from_depth, CameraIntrinsics, ObjectRotation, RenderConfig, RenderHealth,
49 RenderOutput, TargetingPolicy,
50};
51use bevy::prelude::{Transform, Vec3};
52use std::collections::VecDeque;
53use std::path::PathBuf;
54
55#[derive(Clone, Debug)]
57pub struct BatchRenderConfig {
58 pub max_batch_size: usize,
60 pub frame_timeout_ms: u32,
62 pub enable_depth_readback: bool,
64 pub enable_asset_caching: bool,
66 pub resource_cleanup_interval: u32,
68}
69
70impl Default for BatchRenderConfig {
71 fn default() -> Self {
72 Self {
73 max_batch_size: 256,
74 frame_timeout_ms: 500,
75 enable_depth_readback: true,
76 enable_asset_caching: true,
77 resource_cleanup_interval: 32,
78 }
79 }
80}
81
82#[derive(Clone, Debug)]
84pub struct BatchRenderRequest {
85 pub object_dir: PathBuf,
87 pub viewpoint: Transform,
89 pub object_rotation: ObjectRotation,
91 pub render_config: RenderConfig,
93 pub target_point: Vec3,
95 pub targeting_policy: TargetingPolicy,
97}
98
99#[derive(Clone, Debug, Copy, PartialEq, Eq)]
101pub enum RenderStatus {
102 Success,
104 PartialFailure,
106 Failed,
108}
109
110#[derive(Clone, Debug)]
112pub struct BatchRenderOutput {
113 pub request: BatchRenderRequest,
115 pub rgba: Vec<u8>,
117 pub depth: Vec<f64>,
119 pub width: u32,
121 pub height: u32,
123 pub intrinsics: CameraIntrinsics,
125 pub camera_transform: Transform,
127 pub target_point: Vec3,
129 pub targeting_policy: TargetingPolicy,
131 pub health: RenderHealth,
133 pub status: RenderStatus,
135 pub error_message: Option<String>,
137}
138
139impl BatchRenderOutput {
140 pub fn to_rgb_image(&self) -> Vec<Vec<[u8; 3]>> {
142 let mut image = Vec::with_capacity(self.height as usize);
143 for y in 0..self.height {
144 let mut row = Vec::with_capacity(self.width as usize);
145 for x in 0..self.width {
146 let idx = ((y * self.width + x) * 4) as usize;
147 if idx + 2 < self.rgba.len() {
148 row.push([self.rgba[idx], self.rgba[idx + 1], self.rgba[idx + 2]]);
149 } else {
150 row.push([0, 0, 0]);
151 }
152 }
153 image.push(row);
154 }
155 image
156 }
157
158 pub fn to_depth_image(&self) -> Vec<Vec<f64>> {
160 let mut image = Vec::with_capacity(self.height as usize);
161 for y in 0..self.height {
162 let mut row = Vec::with_capacity(self.width as usize);
163 for x in 0..self.width {
164 let idx = (y * self.width + x) as usize;
165 if idx < self.depth.len() {
166 row.push(self.depth[idx]);
167 } else {
168 row.push(0.0);
169 }
170 }
171 image.push(row);
172 }
173 image
174 }
175
176 pub fn semantic_3d(&self, object_semantic_id: u32) -> Vec<[f64; 4]> {
182 self.semantic_3d_with_far_plane(
183 object_semantic_id,
184 self.request.render_config.far_plane as f64,
185 )
186 }
187
188 pub fn semantic_3d_with_far_plane(
190 &self,
191 object_semantic_id: u32,
192 far_plane: f64,
193 ) -> Vec<[f64; 4]> {
194 semantic_3d_from_depth(
195 &self.depth,
196 self.width,
197 self.height,
198 &self.intrinsics,
199 self.camera_transform,
200 object_semantic_id,
201 far_plane,
202 )
203 }
204
205 pub fn from_render_output(request: BatchRenderRequest, output: RenderOutput) -> Self {
207 let health = output.health_with_far_plane(request.render_config.far_plane as f64);
208 let camera_transform = output.camera_transform;
209 let target_point = request.target_point;
210 let targeting_policy = request.targeting_policy.clone();
211 Self {
212 request,
213 rgba: output.rgba,
214 depth: output.depth,
215 width: output.width,
216 height: output.height,
217 intrinsics: output.intrinsics,
218 camera_transform,
219 target_point,
220 targeting_policy,
221 health,
222 status: RenderStatus::Success,
223 error_message: None,
224 }
225 }
226}
227
228#[derive(Debug, Clone)]
230pub enum BatchRenderError {
231 PartialFailure { successful: usize, failed: usize },
233 TotalFailure(String),
235 InvalidConfig(String),
237 QueueFull,
239 EmptyQueue,
241 DeviceLost { reason: String, message: String },
250}
251
252impl std::fmt::Display for BatchRenderError {
253 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254 match self {
255 BatchRenderError::PartialFailure { successful, failed } => {
256 write!(
257 f,
258 "Batch render partial failure: {} succeeded, {} failed",
259 successful, failed
260 )
261 }
262 BatchRenderError::TotalFailure(msg) => write!(f, "Batch render total failure: {}", msg),
263 BatchRenderError::InvalidConfig(msg) => write!(f, "Invalid batch config: {}", msg),
264 BatchRenderError::QueueFull => write!(f, "Batch queue is full"),
265 BatchRenderError::EmptyQueue => write!(f, "No renders queued"),
266 BatchRenderError::DeviceLost { reason, message } => {
267 write!(f, "wgpu device lost ({}): {}", reason, message)
268 }
269 }
270 }
271}
272
273impl std::error::Error for BatchRenderError {}
274
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
277pub enum BatchState {
278 Idle,
280 LoadingAssets,
282 RenderingFrame,
284 ExtractingResults,
286 Cleanup,
288 Shutdown,
290}
291
292pub struct BatchRenderer {
294 pub pending_requests: VecDeque<BatchRenderRequest>,
296 pub completed_results: Vec<BatchRenderOutput>,
298 pub current_request: Option<BatchRenderRequest>,
300 pub current_output: Option<BatchRenderOutput>,
302 pub frame_count: u32,
304 pub state: BatchState,
306 pub config: BatchRenderConfig,
308 pub renders_processed: usize,
310}
311
312impl BatchRenderer {
313 pub fn new(config: BatchRenderConfig) -> Self {
315 Self {
316 pending_requests: VecDeque::new(),
317 completed_results: Vec::new(),
318 current_request: None,
319 current_output: None,
320 frame_count: 0,
321 state: BatchState::Idle,
322 config,
323 renders_processed: 0,
324 }
325 }
326
327 pub fn queue_request(&mut self, request: BatchRenderRequest) -> Result<(), BatchRenderError> {
329 if self.pending_requests.len() >= self.config.max_batch_size {
330 return Err(BatchRenderError::QueueFull);
331 }
332 self.pending_requests.push_back(request);
333 Ok(())
334 }
335
336 pub fn pending_count(&self) -> usize {
338 self.pending_requests.len()
339 }
340
341 pub fn completed_count(&self) -> usize {
343 self.completed_results.len()
344 }
345
346 pub fn take_completed(&mut self) -> Vec<BatchRenderOutput> {
348 std::mem::take(&mut self.completed_results)
349 }
350
351 pub fn is_finished(&self) -> bool {
353 self.pending_requests.is_empty() && self.current_request.is_none()
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_batch_config_defaults() {
363 let config = BatchRenderConfig::default();
364 assert_eq!(config.max_batch_size, 256);
365 assert_eq!(config.frame_timeout_ms, 500);
366 assert!(config.enable_depth_readback);
367 assert!(config.enable_asset_caching);
368 }
369
370 #[test]
371 fn test_batch_renderer_creation() {
372 let config = BatchRenderConfig::default();
373 let renderer = BatchRenderer::new(config);
374 assert_eq!(renderer.state, BatchState::Idle);
375 assert_eq!(renderer.pending_count(), 0);
376 assert_eq!(renderer.completed_count(), 0);
377 assert!(renderer.is_finished());
378 }
379
380 #[test]
381 fn test_queue_request() {
382 let mut renderer = BatchRenderer::new(BatchRenderConfig::default());
383 let request = BatchRenderRequest {
384 object_dir: "/tmp/test".into(),
385 viewpoint: Transform::default(),
386 object_rotation: ObjectRotation::identity(),
387 render_config: RenderConfig::tbp_default(),
388 target_point: Vec3::ZERO,
389 targeting_policy: TargetingPolicy::Origin,
390 };
391 assert!(renderer.queue_request(request).is_ok());
392 assert_eq!(renderer.pending_count(), 1);
393 }
394
395 #[test]
396 fn test_queue_full() {
397 let config = BatchRenderConfig {
398 max_batch_size: 1,
399 ..BatchRenderConfig::default()
400 };
401 let mut renderer = BatchRenderer::new(config);
402
403 let request = BatchRenderRequest {
404 object_dir: "/tmp/test".into(),
405 viewpoint: Transform::default(),
406 object_rotation: ObjectRotation::identity(),
407 render_config: RenderConfig::tbp_default(),
408 target_point: Vec3::ZERO,
409 targeting_policy: TargetingPolicy::Origin,
410 };
411
412 assert!(renderer.queue_request(request.clone()).is_ok());
413 assert!(matches!(
414 renderer.queue_request(request),
415 Err(BatchRenderError::QueueFull)
416 ));
417 }
418
419 #[test]
420 fn test_batch_render_output_rgb_conversion() {
421 let request = BatchRenderRequest {
422 object_dir: "/tmp/test".into(),
423 viewpoint: Transform::default(),
424 object_rotation: ObjectRotation::identity(),
425 render_config: RenderConfig::tbp_default(),
426 target_point: Vec3::ZERO,
427 targeting_policy: TargetingPolicy::Origin,
428 };
429
430 let mut rgba = vec![0u8; 2 * 2 * 4];
432 rgba[0] = 255;
434 rgba[1] = 0;
435 rgba[2] = 0;
436 rgba[3] = 255;
437
438 let output = BatchRenderOutput {
439 request,
440 rgba,
441 depth: vec![1.0; 4],
442 width: 2,
443 height: 2,
444 intrinsics: RenderConfig::tbp_default().intrinsics(),
445 camera_transform: Transform::default(),
446 target_point: Vec3::ZERO,
447 targeting_policy: TargetingPolicy::Origin,
448 health: RenderHealth {
449 center_pixel: Some([1, 1]),
450 center_depth: Some(1.0),
451 center_foreground: true,
452 foreground_pixel_count: 4,
453 foreground_coverage: 1.0,
454 center_5x5_foreground_count: 4,
455 nearest_foreground_pixel: Some([1, 1]),
456 nearest_foreground_depth: Some(1.0),
457 nearest_foreground_distance_px: Some(0.0),
458 },
459 status: RenderStatus::Success,
460 error_message: None,
461 };
462
463 let rgb = output.to_rgb_image();
464 assert_eq!(rgb.len(), 2); assert_eq!(rgb[0].len(), 2); assert_eq!(rgb[0][0], [255, 0, 0]); }
468
469 #[test]
470 fn test_batch_render_output_carries_request_target_metadata() {
471 let target_point = Vec3::new(0.25, -0.125, 0.5);
472 let camera_transform = Transform::from_xyz(0.0, 0.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y);
473 let request = BatchRenderRequest {
474 object_dir: "/tmp/test".into(),
475 viewpoint: camera_transform,
476 object_rotation: ObjectRotation::identity(),
477 render_config: RenderConfig::tbp_default(),
478 target_point,
479 targeting_policy: TargetingPolicy::MeshCenter,
480 };
481 let output = RenderOutput {
482 rgba: vec![0u8; 4],
483 depth: vec![1.0],
484 width: 1,
485 height: 1,
486 intrinsics: RenderConfig::tbp_default().intrinsics(),
487 camera_transform,
488 object_rotation: ObjectRotation::identity(),
489 target_point: Vec3::ZERO,
490 targeting_policy: TargetingPolicy::Origin,
491 };
492
493 let batch_output = BatchRenderOutput::from_render_output(request, output);
494
495 assert_eq!(batch_output.target_point, target_point);
496 assert_eq!(batch_output.targeting_policy, TargetingPolicy::MeshCenter);
497 assert_eq!(batch_output.camera_transform, camera_transform);
498 assert_eq!(batch_output.request.target_point, target_point);
499 assert_eq!(
500 batch_output.request.targeting_policy,
501 TargetingPolicy::MeshCenter
502 );
503 }
504
505 #[test]
506 fn test_batch_render_output_semantic_3d_uses_camera_transform() {
507 let camera_transform = Transform::from_xyz(0.0, 0.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y);
508 let request = BatchRenderRequest {
509 object_dir: "/tmp/test".into(),
510 viewpoint: camera_transform,
511 object_rotation: ObjectRotation::identity(),
512 render_config: RenderConfig::tbp_default(),
513 target_point: Vec3::ZERO,
514 targeting_policy: TargetingPolicy::Origin,
515 };
516 let output = RenderOutput {
517 rgba: vec![0u8; 4],
518 depth: vec![1.5],
519 width: 1,
520 height: 1,
521 intrinsics: CameraIntrinsics {
522 focal_length: [100.0, 100.0],
523 principal_point: [0.0, 0.0],
524 image_size: [1, 1],
525 },
526 camera_transform,
527 object_rotation: ObjectRotation::identity(),
528 target_point: Vec3::ZERO,
529 targeting_policy: TargetingPolicy::Origin,
530 };
531
532 let batch_output = BatchRenderOutput::from_render_output(request, output);
533 let rows = batch_output.semantic_3d(7);
534
535 assert_eq!(rows.len(), 1);
536 assert!((rows[0][0]).abs() < 1e-6);
537 assert!((rows[0][1]).abs() < 1e-6);
538 assert!((rows[0][2] - 0.5).abs() < 1e-6);
539 assert_eq!(rows[0][3], 7.0);
540 }
541}