use crate::{CameraIntrinsics, ObjectRotation, RenderConfig, RenderOutput};
use bevy::prelude::Transform;
use std::collections::VecDeque;
use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct BatchRenderConfig {
pub max_batch_size: usize,
pub frame_timeout_ms: u32,
pub enable_depth_readback: bool,
pub enable_asset_caching: bool,
pub resource_cleanup_interval: u32,
}
impl Default for BatchRenderConfig {
fn default() -> Self {
Self {
max_batch_size: 256,
frame_timeout_ms: 500,
enable_depth_readback: true,
enable_asset_caching: true,
resource_cleanup_interval: 32,
}
}
}
#[derive(Clone, Debug)]
pub struct BatchRenderRequest {
pub object_dir: PathBuf,
pub viewpoint: Transform,
pub object_rotation: ObjectRotation,
pub render_config: RenderConfig,
}
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum RenderStatus {
Success,
PartialFailure,
Failed,
}
#[derive(Clone, Debug)]
pub struct BatchRenderOutput {
pub request: BatchRenderRequest,
pub rgba: Vec<u8>,
pub depth: Vec<f64>,
pub width: u32,
pub height: u32,
pub intrinsics: CameraIntrinsics,
pub status: RenderStatus,
pub error_message: Option<String>,
}
impl BatchRenderOutput {
pub fn to_rgb_image(&self) -> Vec<Vec<[u8; 3]>> {
let mut image = Vec::with_capacity(self.height as usize);
for y in 0..self.height {
let mut row = Vec::with_capacity(self.width as usize);
for x in 0..self.width {
let idx = ((y * self.width + x) * 4) as usize;
if idx + 2 < self.rgba.len() {
row.push([self.rgba[idx], self.rgba[idx + 1], self.rgba[idx + 2]]);
} else {
row.push([0, 0, 0]);
}
}
image.push(row);
}
image
}
pub fn to_depth_image(&self) -> Vec<Vec<f64>> {
let mut image = Vec::with_capacity(self.height as usize);
for y in 0..self.height {
let mut row = Vec::with_capacity(self.width as usize);
for x in 0..self.width {
let idx = (y * self.width + x) as usize;
if idx < self.depth.len() {
row.push(self.depth[idx]);
} else {
row.push(0.0);
}
}
image.push(row);
}
image
}
pub fn from_render_output(request: BatchRenderRequest, output: RenderOutput) -> Self {
Self {
request,
rgba: output.rgba,
depth: output.depth,
width: output.width,
height: output.height,
intrinsics: output.intrinsics,
status: RenderStatus::Success,
error_message: None,
}
}
}
#[derive(Debug, Clone)]
pub enum BatchRenderError {
PartialFailure { successful: usize, failed: usize },
TotalFailure(String),
InvalidConfig(String),
QueueFull,
EmptyQueue,
DeviceLost { reason: String, message: String },
}
impl std::fmt::Display for BatchRenderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BatchRenderError::PartialFailure { successful, failed } => {
write!(
f,
"Batch render partial failure: {} succeeded, {} failed",
successful, failed
)
}
BatchRenderError::TotalFailure(msg) => write!(f, "Batch render total failure: {}", msg),
BatchRenderError::InvalidConfig(msg) => write!(f, "Invalid batch config: {}", msg),
BatchRenderError::QueueFull => write!(f, "Batch queue is full"),
BatchRenderError::EmptyQueue => write!(f, "No renders queued"),
BatchRenderError::DeviceLost { reason, message } => {
write!(f, "wgpu device lost ({}): {}", reason, message)
}
}
}
}
impl std::error::Error for BatchRenderError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BatchState {
Idle,
LoadingAssets,
RenderingFrame,
ExtractingResults,
Cleanup,
Shutdown,
}
pub struct BatchRenderer {
pub pending_requests: VecDeque<BatchRenderRequest>,
pub completed_results: Vec<BatchRenderOutput>,
pub current_request: Option<BatchRenderRequest>,
pub current_output: Option<BatchRenderOutput>,
pub frame_count: u32,
pub state: BatchState,
pub config: BatchRenderConfig,
pub renders_processed: usize,
}
impl BatchRenderer {
pub fn new(config: BatchRenderConfig) -> Self {
Self {
pending_requests: VecDeque::new(),
completed_results: Vec::new(),
current_request: None,
current_output: None,
frame_count: 0,
state: BatchState::Idle,
config,
renders_processed: 0,
}
}
pub fn queue_request(&mut self, request: BatchRenderRequest) -> Result<(), BatchRenderError> {
if self.pending_requests.len() >= self.config.max_batch_size {
return Err(BatchRenderError::QueueFull);
}
self.pending_requests.push_back(request);
Ok(())
}
pub fn pending_count(&self) -> usize {
self.pending_requests.len()
}
pub fn completed_count(&self) -> usize {
self.completed_results.len()
}
pub fn take_completed(&mut self) -> Vec<BatchRenderOutput> {
std::mem::take(&mut self.completed_results)
}
pub fn is_finished(&self) -> bool {
self.pending_requests.is_empty() && self.current_request.is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_config_defaults() {
let config = BatchRenderConfig::default();
assert_eq!(config.max_batch_size, 256);
assert_eq!(config.frame_timeout_ms, 500);
assert!(config.enable_depth_readback);
assert!(config.enable_asset_caching);
}
#[test]
fn test_batch_renderer_creation() {
let config = BatchRenderConfig::default();
let renderer = BatchRenderer::new(config);
assert_eq!(renderer.state, BatchState::Idle);
assert_eq!(renderer.pending_count(), 0);
assert_eq!(renderer.completed_count(), 0);
assert!(renderer.is_finished());
}
#[test]
fn test_queue_request() {
let mut renderer = BatchRenderer::new(BatchRenderConfig::default());
let request = BatchRenderRequest {
object_dir: "/tmp/test".into(),
viewpoint: Transform::default(),
object_rotation: ObjectRotation::identity(),
render_config: RenderConfig::tbp_default(),
};
assert!(renderer.queue_request(request).is_ok());
assert_eq!(renderer.pending_count(), 1);
}
#[test]
fn test_queue_full() {
let config = BatchRenderConfig {
max_batch_size: 1,
..BatchRenderConfig::default()
};
let mut renderer = BatchRenderer::new(config);
let request = BatchRenderRequest {
object_dir: "/tmp/test".into(),
viewpoint: Transform::default(),
object_rotation: ObjectRotation::identity(),
render_config: RenderConfig::tbp_default(),
};
assert!(renderer.queue_request(request.clone()).is_ok());
assert!(matches!(
renderer.queue_request(request),
Err(BatchRenderError::QueueFull)
));
}
#[test]
fn test_batch_render_output_rgb_conversion() {
let request = BatchRenderRequest {
object_dir: "/tmp/test".into(),
viewpoint: Transform::default(),
object_rotation: ObjectRotation::identity(),
render_config: RenderConfig::tbp_default(),
};
let mut rgba = vec![0u8; 2 * 2 * 4];
rgba[0] = 255;
rgba[1] = 0;
rgba[2] = 0;
rgba[3] = 255;
let output = BatchRenderOutput {
request,
rgba,
depth: vec![1.0; 4],
width: 2,
height: 2,
intrinsics: RenderConfig::tbp_default().intrinsics(),
status: RenderStatus::Success,
error_message: None,
};
let rgb = output.to_rgb_image();
assert_eq!(rgb.len(), 2); assert_eq!(rgb[0].len(), 2); assert_eq!(rgb[0][0], [255, 0, 0]); }
}