1use crate::{CameraIntrinsics, ObjectRotation, RenderConfig, RenderOutput};
45use bevy::prelude::Transform;
46use std::collections::VecDeque;
47use std::path::PathBuf;
48
49#[derive(Clone, Debug)]
51pub struct BatchRenderConfig {
52 pub max_batch_size: usize,
54 pub frame_timeout_ms: u32,
56 pub enable_depth_readback: bool,
58 pub enable_asset_caching: bool,
60 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#[derive(Clone, Debug)]
78pub struct BatchRenderRequest {
79 pub object_dir: PathBuf,
81 pub viewpoint: Transform,
83 pub object_rotation: ObjectRotation,
85 pub render_config: RenderConfig,
87}
88
89#[derive(Clone, Debug, Copy, PartialEq, Eq)]
91pub enum RenderStatus {
92 Success,
94 PartialFailure,
96 Failed,
98}
99
100#[derive(Clone, Debug)]
102pub struct BatchRenderOutput {
103 pub request: BatchRenderRequest,
105 pub rgba: Vec<u8>,
107 pub depth: Vec<f64>,
109 pub width: u32,
111 pub height: u32,
113 pub intrinsics: CameraIntrinsics,
115 pub status: RenderStatus,
117 pub error_message: Option<String>,
119}
120
121impl BatchRenderOutput {
122 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 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 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#[derive(Debug, Clone)]
175pub enum BatchRenderError {
176 PartialFailure { successful: usize, failed: usize },
178 TotalFailure(String),
180 InvalidConfig(String),
182 QueueFull,
184 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub enum BatchState {
211 Idle,
213 LoadingAssets,
215 RenderingFrame,
217 ExtractingResults,
219 Cleanup,
221 Shutdown,
223}
224
225pub struct BatchRenderer {
227 pub pending_requests: VecDeque<BatchRenderRequest>,
229 pub completed_results: Vec<BatchRenderOutput>,
231 pub current_request: Option<BatchRenderRequest>,
233 pub current_output: Option<BatchRenderOutput>,
235 pub frame_count: u32,
237 pub state: BatchState,
239 pub config: BatchRenderConfig,
241 pub renders_processed: usize,
243}
244
245impl BatchRenderer {
246 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 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 pub fn pending_count(&self) -> usize {
271 self.pending_requests.len()
272 }
273
274 pub fn completed_count(&self) -> usize {
276 self.completed_results.len()
277 }
278
279 pub fn take_completed(&mut self) -> Vec<BatchRenderOutput> {
281 std::mem::take(&mut self.completed_results)
282 }
283
284 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 let mut rgba = vec![0u8; 2 * 2 * 4];
359 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); assert_eq!(rgb[0].len(), 2); assert_eq!(rgb[0][0], [255, 0, 0]); }
381}