Skip to main content

bevy_sensor/
batch.rs

1//! Batch rendering API for multiple viewpoints and objects.
2//!
3//! Today this module is a queue-oriented wrapper around sequential `render_to_buffer()`
4//! calls. It does not yet keep a persistent Bevy app alive across renders; that follow-up
5//! remains tracked work. The API is still useful for consumers that want ordered request
6//! management and structured batch outputs without promising reuse semantics that do not
7//! exist yet.
8//!
9//! # Example
10//!
11//! ```ignore
12//! use bevy_sensor::{
13//!     create_batch_renderer, queue_render_request, render_next_in_batch,
14//!     batch::BatchRenderRequest, BatchRenderConfig, RenderConfig, ObjectRotation,
15//!     TargetingPolicy, Vec3,
16//! };
17//! use std::path::PathBuf;
18//!
19//! // Create a batch helper
20//! let config = BatchRenderConfig::default();
21//! let mut renderer = create_batch_renderer(&config)?;
22//!
23//! // Queue multiple renders
24//! for rotation in rotations {
25//!     for viewpoint in viewpoints {
26//!         queue_render_request(&mut renderer, BatchRenderRequest {
27//!             object_dir: "/tmp/ycb/003_cracker_box".into(),
28//!             viewpoint,
29//!             object_rotation: rotation.clone(),
30//!             render_config: RenderConfig::tbp_default(),
31//!             target_point: Vec3::ZERO,
32//!             targeting_policy: TargetingPolicy::Origin,
33//!         })?;
34//!     }
35//! }
36//!
37//! // Execute and collect results
38//! let mut results = Vec::new();
39//! loop {
40//!     match render_next_in_batch(&mut renderer, 500)? {
41//!         Some(output) => results.push(output),
42//!         None => break,
43//!     }
44//! }
45//! ```
46
47use crate::{
48    CameraIntrinsics, ObjectRotation, RenderConfig, RenderHealth, RenderOutput, TargetingPolicy,
49};
50use bevy::prelude::{Transform, Vec3};
51use std::collections::VecDeque;
52use std::path::PathBuf;
53
54/// Configuration for batch rendering.
55#[derive(Clone, Debug)]
56pub struct BatchRenderConfig {
57    /// Maximum number of renders to queue before automatic cleanup
58    pub max_batch_size: usize,
59    /// Timeout in milliseconds per individual render
60    pub frame_timeout_ms: u32,
61    /// Enable depth buffer readback
62    pub enable_depth_readback: bool,
63    /// Enable asset caching for repeated objects
64    pub enable_asset_caching: bool,
65    /// Number of renders before triggering resource cleanup
66    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/// A single render request in a batch.
82#[derive(Clone, Debug)]
83pub struct BatchRenderRequest {
84    /// Path to YCB object directory (e.g., "/tmp/ycb/003_cracker_box")
85    pub object_dir: PathBuf,
86    /// Camera transform (position and orientation)
87    pub viewpoint: Transform,
88    /// Object rotation to apply
89    pub object_rotation: ObjectRotation,
90    /// Render configuration (resolution, lighting, etc.)
91    pub render_config: RenderConfig,
92    /// Point the camera was intended to target for this render.
93    pub target_point: Vec3,
94    /// Policy used to derive `target_point`.
95    pub targeting_policy: TargetingPolicy,
96}
97
98/// Status of a single render in a batch.
99#[derive(Clone, Debug, Copy, PartialEq, Eq)]
100pub enum RenderStatus {
101    /// Render completed successfully with RGBA and depth
102    Success,
103    /// Render completed but depth extraction failed
104    PartialFailure,
105    /// Render failed completely
106    Failed,
107}
108
109/// Output from a single render in a batch.
110#[derive(Clone, Debug)]
111pub struct BatchRenderOutput {
112    /// Original request for this render
113    pub request: BatchRenderRequest,
114    /// RGBA pixel data (width * height * 4 bytes, row-major)
115    pub rgba: Vec<u8>,
116    /// Depth data in meters (width * height f64s)
117    pub depth: Vec<f64>,
118    /// Image width in pixels
119    pub width: u32,
120    /// Image height in pixels
121    pub height: u32,
122    /// Camera intrinsics used
123    pub intrinsics: CameraIntrinsics,
124    /// Point the camera was intended to target for this render.
125    pub target_point: Vec3,
126    /// Policy used to derive `target_point`.
127    pub targeting_policy: TargetingPolicy,
128    /// Cheap diagnostics derived from the rendered depth buffer
129    pub health: RenderHealth,
130    /// Status of this render
131    pub status: RenderStatus,
132    /// Error message if status is Failed or PartialFailure
133    pub error_message: Option<String>,
134}
135
136impl BatchRenderOutput {
137    /// Convert to neocortx-compatible RGB format: Vec<Vec<[u8; 3]>>
138    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    /// Convert depth to neocortx-compatible format: Vec<Vec<f64>>
156    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    /// Convert from RenderOutput, carrying request-level target metadata.
174    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/// Error types for batch rendering.
195#[derive(Debug, Clone)]
196pub enum BatchRenderError {
197    /// Some renders succeeded, others failed
198    PartialFailure { successful: usize, failed: usize },
199    /// All renders failed
200    TotalFailure(String),
201    /// Invalid configuration
202    InvalidConfig(String),
203    /// Queue is full
204    QueueFull,
205    /// No renders queued
206    EmptyQueue,
207    /// The wgpu device was lost mid-render. The current `RenderSession::render()`
208    /// call produced no output; any outputs returned by earlier calls remain valid.
209    /// Recovery: drop the session and construct a new one.
210    ///
211    /// `reason` is a string form of `wgpu::DeviceLostReason` so callers can branch
212    /// on recoverable vs. adapter-evicted without taking a direct wgpu dependency.
213    /// Phase 1 ships the string form; a typed variant may follow once the Bevy
214    /// re-export surface is clearer.
215    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/// State machine for batch rendering lifecycle.
242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243pub enum BatchState {
244    /// Idle, waiting for requests to queue
245    Idle,
246    /// Loading object assets (mesh, texture)
247    LoadingAssets,
248    /// Rendering frame to GPU buffer
249    RenderingFrame,
250    /// Extracting RGBA and depth from GPU
251    ExtractingResults,
252    /// Cleaning up resources
253    Cleanup,
254    /// Shutting down
255    Shutdown,
256}
257
258/// Manages queued render requests and completed outputs for batch-style workflows.
259pub struct BatchRenderer {
260    /// Queued render requests
261    pub pending_requests: VecDeque<BatchRenderRequest>,
262    /// Completed results
263    pub completed_results: Vec<BatchRenderOutput>,
264    /// Current request being processed
265    pub current_request: Option<BatchRenderRequest>,
266    /// Current render output being built
267    pub current_output: Option<BatchRenderOutput>,
268    /// Frame counter for timeout management
269    pub frame_count: u32,
270    /// Current state
271    pub state: BatchState,
272    /// Configuration
273    pub config: BatchRenderConfig,
274    /// Total renders processed
275    pub renders_processed: usize,
276}
277
278impl BatchRenderer {
279    /// Create a new batch renderer with default configuration.
280    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    /// Queue a render request for batch processing.
294    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    /// Get the number of pending requests.
303    pub fn pending_count(&self) -> usize {
304        self.pending_requests.len()
305    }
306
307    /// Get the number of completed results.
308    pub fn completed_count(&self) -> usize {
309        self.completed_results.len()
310    }
311
312    /// Get all completed results and clear the internal list.
313    pub fn take_completed(&mut self) -> Vec<BatchRenderOutput> {
314        std::mem::take(&mut self.completed_results)
315    }
316
317    /// Check if all work is done (no pending requests and not currently rendering).
318    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        // Create minimal output: 2x2 image
397        let mut rgba = vec![0u8; 2 * 2 * 4];
398        // Pixel (0,0) = red
399        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); // 2 rows
430        assert_eq!(rgb[0].len(), 2); // 2 cols
431        assert_eq!(rgb[0][0], [255, 0, 0]); // Red
432    }
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}