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//! };
16//! use std::path::PathBuf;
17//!
18//! // Create a batch helper
19//! let config = BatchRenderConfig::default();
20//! let mut renderer = create_batch_renderer(&config)?;
21//!
22//! // Queue multiple renders
23//! for rotation in rotations {
24//!     for viewpoint in viewpoints {
25//!         queue_render_request(&mut renderer, BatchRenderRequest {
26//!             object_dir: "/tmp/ycb/003_cracker_box".into(),
27//!             viewpoint,
28//!             object_rotation: rotation.clone(),
29//!             render_config: RenderConfig::tbp_default(),
30//!         })?;
31//!     }
32//! }
33//!
34//! // Execute and collect results
35//! let mut results = Vec::new();
36//! loop {
37//!     match render_next_in_batch(&mut renderer, 500)? {
38//!         Some(output) => results.push(output),
39//!         None => break,
40//!     }
41//! }
42//! ```
43
44use crate::{CameraIntrinsics, ObjectRotation, RenderConfig, RenderOutput};
45use bevy::prelude::Transform;
46use std::collections::VecDeque;
47use std::path::PathBuf;
48
49/// Configuration for batch rendering.
50#[derive(Clone, Debug)]
51pub struct BatchRenderConfig {
52    /// Maximum number of renders to queue before automatic cleanup
53    pub max_batch_size: usize,
54    /// Timeout in milliseconds per individual render
55    pub frame_timeout_ms: u32,
56    /// Enable depth buffer readback
57    pub enable_depth_readback: bool,
58    /// Enable asset caching for repeated objects
59    pub enable_asset_caching: bool,
60    /// Number of renders before triggering resource cleanup
61    pub resource_cleanup_interval: u32,
62}
63
64impl Default for BatchRenderConfig {
65    fn default() -> Self {
66        Self {
67            max_batch_size: 256,
68            frame_timeout_ms: 500,
69            enable_depth_readback: true,
70            enable_asset_caching: true,
71            resource_cleanup_interval: 32,
72        }
73    }
74}
75
76/// A single render request in a batch.
77#[derive(Clone, Debug)]
78pub struct BatchRenderRequest {
79    /// Path to YCB object directory (e.g., "/tmp/ycb/003_cracker_box")
80    pub object_dir: PathBuf,
81    /// Camera transform (position and orientation)
82    pub viewpoint: Transform,
83    /// Object rotation to apply
84    pub object_rotation: ObjectRotation,
85    /// Render configuration (resolution, lighting, etc.)
86    pub render_config: RenderConfig,
87}
88
89/// Status of a single render in a batch.
90#[derive(Clone, Debug, Copy, PartialEq, Eq)]
91pub enum RenderStatus {
92    /// Render completed successfully with RGBA and depth
93    Success,
94    /// Render completed but depth extraction failed
95    PartialFailure,
96    /// Render failed completely
97    Failed,
98}
99
100/// Output from a single render in a batch.
101#[derive(Clone, Debug)]
102pub struct BatchRenderOutput {
103    /// Original request for this render
104    pub request: BatchRenderRequest,
105    /// RGBA pixel data (width * height * 4 bytes, row-major)
106    pub rgba: Vec<u8>,
107    /// Depth data in meters (width * height f64s)
108    pub depth: Vec<f64>,
109    /// Image width in pixels
110    pub width: u32,
111    /// Image height in pixels
112    pub height: u32,
113    /// Camera intrinsics used
114    pub intrinsics: CameraIntrinsics,
115    /// Status of this render
116    pub status: RenderStatus,
117    /// Error message if status is Failed or PartialFailure
118    pub error_message: Option<String>,
119}
120
121impl BatchRenderOutput {
122    /// Convert to neocortx-compatible RGB format: Vec<Vec<[u8; 3]>>
123    pub fn to_rgb_image(&self) -> Vec<Vec<[u8; 3]>> {
124        let mut image = Vec::with_capacity(self.height as usize);
125        for y in 0..self.height {
126            let mut row = Vec::with_capacity(self.width as usize);
127            for x in 0..self.width {
128                let idx = ((y * self.width + x) * 4) as usize;
129                if idx + 2 < self.rgba.len() {
130                    row.push([self.rgba[idx], self.rgba[idx + 1], self.rgba[idx + 2]]);
131                } else {
132                    row.push([0, 0, 0]);
133                }
134            }
135            image.push(row);
136        }
137        image
138    }
139
140    /// Convert depth to neocortx-compatible format: Vec<Vec<f64>>
141    pub fn to_depth_image(&self) -> Vec<Vec<f64>> {
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) as usize;
147                if idx < self.depth.len() {
148                    row.push(self.depth[idx]);
149                } else {
150                    row.push(0.0);
151                }
152            }
153            image.push(row);
154        }
155        image
156    }
157
158    /// Convert from RenderOutput, copying all fields
159    pub fn from_render_output(request: BatchRenderRequest, output: RenderOutput) -> Self {
160        Self {
161            request,
162            rgba: output.rgba,
163            depth: output.depth,
164            width: output.width,
165            height: output.height,
166            intrinsics: output.intrinsics,
167            status: RenderStatus::Success,
168            error_message: None,
169        }
170    }
171}
172
173/// Error types for batch rendering.
174#[derive(Debug, Clone)]
175pub enum BatchRenderError {
176    /// Some renders succeeded, others failed
177    PartialFailure { successful: usize, failed: usize },
178    /// All renders failed
179    TotalFailure(String),
180    /// Invalid configuration
181    InvalidConfig(String),
182    /// Queue is full
183    QueueFull,
184    /// No renders queued
185    EmptyQueue,
186}
187
188impl std::fmt::Display for BatchRenderError {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        match self {
191            BatchRenderError::PartialFailure { successful, failed } => {
192                write!(
193                    f,
194                    "Batch render partial failure: {} succeeded, {} failed",
195                    successful, failed
196                )
197            }
198            BatchRenderError::TotalFailure(msg) => write!(f, "Batch render total failure: {}", msg),
199            BatchRenderError::InvalidConfig(msg) => write!(f, "Invalid batch config: {}", msg),
200            BatchRenderError::QueueFull => write!(f, "Batch queue is full"),
201            BatchRenderError::EmptyQueue => write!(f, "No renders queued"),
202        }
203    }
204}
205
206impl std::error::Error for BatchRenderError {}
207
208/// State machine for batch rendering lifecycle.
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub enum BatchState {
211    /// Idle, waiting for requests to queue
212    Idle,
213    /// Loading object assets (mesh, texture)
214    LoadingAssets,
215    /// Rendering frame to GPU buffer
216    RenderingFrame,
217    /// Extracting RGBA and depth from GPU
218    ExtractingResults,
219    /// Cleaning up resources
220    Cleanup,
221    /// Shutting down
222    Shutdown,
223}
224
225/// Manages queued render requests and completed outputs for batch-style workflows.
226pub struct BatchRenderer {
227    /// Queued render requests
228    pub pending_requests: VecDeque<BatchRenderRequest>,
229    /// Completed results
230    pub completed_results: Vec<BatchRenderOutput>,
231    /// Current request being processed
232    pub current_request: Option<BatchRenderRequest>,
233    /// Current render output being built
234    pub current_output: Option<BatchRenderOutput>,
235    /// Frame counter for timeout management
236    pub frame_count: u32,
237    /// Current state
238    pub state: BatchState,
239    /// Configuration
240    pub config: BatchRenderConfig,
241    /// Total renders processed
242    pub renders_processed: usize,
243}
244
245impl BatchRenderer {
246    /// Create a new batch renderer with default configuration.
247    pub fn new(config: BatchRenderConfig) -> Self {
248        Self {
249            pending_requests: VecDeque::new(),
250            completed_results: Vec::new(),
251            current_request: None,
252            current_output: None,
253            frame_count: 0,
254            state: BatchState::Idle,
255            config,
256            renders_processed: 0,
257        }
258    }
259
260    /// Queue a render request for batch processing.
261    pub fn queue_request(&mut self, request: BatchRenderRequest) -> Result<(), BatchRenderError> {
262        if self.pending_requests.len() >= self.config.max_batch_size {
263            return Err(BatchRenderError::QueueFull);
264        }
265        self.pending_requests.push_back(request);
266        Ok(())
267    }
268
269    /// Get the number of pending requests.
270    pub fn pending_count(&self) -> usize {
271        self.pending_requests.len()
272    }
273
274    /// Get the number of completed results.
275    pub fn completed_count(&self) -> usize {
276        self.completed_results.len()
277    }
278
279    /// Get all completed results and clear the internal list.
280    pub fn take_completed(&mut self) -> Vec<BatchRenderOutput> {
281        std::mem::take(&mut self.completed_results)
282    }
283
284    /// Check if all work is done (no pending requests and not currently rendering).
285    pub fn is_finished(&self) -> bool {
286        self.pending_requests.is_empty() && self.current_request.is_none()
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_batch_config_defaults() {
296        let config = BatchRenderConfig::default();
297        assert_eq!(config.max_batch_size, 256);
298        assert_eq!(config.frame_timeout_ms, 500);
299        assert!(config.enable_depth_readback);
300        assert!(config.enable_asset_caching);
301    }
302
303    #[test]
304    fn test_batch_renderer_creation() {
305        let config = BatchRenderConfig::default();
306        let renderer = BatchRenderer::new(config);
307        assert_eq!(renderer.state, BatchState::Idle);
308        assert_eq!(renderer.pending_count(), 0);
309        assert_eq!(renderer.completed_count(), 0);
310        assert!(renderer.is_finished());
311    }
312
313    #[test]
314    fn test_queue_request() {
315        let mut renderer = BatchRenderer::new(BatchRenderConfig::default());
316        let request = BatchRenderRequest {
317            object_dir: "/tmp/test".into(),
318            viewpoint: Transform::default(),
319            object_rotation: ObjectRotation::identity(),
320            render_config: RenderConfig::tbp_default(),
321        };
322        assert!(renderer.queue_request(request).is_ok());
323        assert_eq!(renderer.pending_count(), 1);
324    }
325
326    #[test]
327    fn test_queue_full() {
328        let config = BatchRenderConfig {
329            max_batch_size: 1,
330            ..BatchRenderConfig::default()
331        };
332        let mut renderer = BatchRenderer::new(config);
333
334        let request = BatchRenderRequest {
335            object_dir: "/tmp/test".into(),
336            viewpoint: Transform::default(),
337            object_rotation: ObjectRotation::identity(),
338            render_config: RenderConfig::tbp_default(),
339        };
340
341        assert!(renderer.queue_request(request.clone()).is_ok());
342        assert!(matches!(
343            renderer.queue_request(request),
344            Err(BatchRenderError::QueueFull)
345        ));
346    }
347
348    #[test]
349    fn test_batch_render_output_rgb_conversion() {
350        let request = BatchRenderRequest {
351            object_dir: "/tmp/test".into(),
352            viewpoint: Transform::default(),
353            object_rotation: ObjectRotation::identity(),
354            render_config: RenderConfig::tbp_default(),
355        };
356
357        // Create minimal output: 2x2 image
358        let mut rgba = vec![0u8; 2 * 2 * 4];
359        // Pixel (0,0) = red
360        rgba[0] = 255;
361        rgba[1] = 0;
362        rgba[2] = 0;
363        rgba[3] = 255;
364
365        let output = BatchRenderOutput {
366            request,
367            rgba,
368            depth: vec![1.0; 4],
369            width: 2,
370            height: 2,
371            intrinsics: RenderConfig::tbp_default().intrinsics(),
372            status: RenderStatus::Success,
373            error_message: None,
374        };
375
376        let rgb = output.to_rgb_image();
377        assert_eq!(rgb.len(), 2); // 2 rows
378        assert_eq!(rgb[0].len(), 2); // 2 cols
379        assert_eq!(rgb[0][0], [255, 0, 0]); // Red
380    }
381}