use bevy_sensor::{
backend::detect_platform, batch::BatchRenderRequest, cache::ModelCache, render_batch,
render_to_buffer, render_to_buffer_cached, BatchRenderConfig, ObjectRotation, RenderConfig,
RenderOutput, ViewpointConfig,
};
use std::fs;
use std::path::PathBuf;
use std::time::Instant;
fn save_render_output(output: &RenderOutput, name: &str) -> Result<(), Box<dyn std::error::Error>> {
let render_dir = PathBuf::from("test_fixtures/test_renders");
fs::create_dir_all(&render_dir)?;
let rgb_image = output.to_rgb_image();
let img = image::ImageBuffer::from_fn(output.width, output.height, |x, y| {
let rgb = rgb_image[y as usize][x as usize];
image::Rgb(rgb)
});
let rgba_path = render_dir.join(format!("{}.png", name));
img.save(&rgba_path)?;
println!(" Saved RGBA: {}", rgba_path.display());
let depth_path = render_dir.join(format!("{}.depth", name));
let depth_bytes: Vec<u8> = output.depth.iter().flat_map(|&d| d.to_le_bytes()).collect();
fs::write(&depth_path, &depth_bytes)?;
println!(" Saved depth: {}", depth_path.display());
Ok(())
}
#[test]
#[ignore] fn test_render_integration() {
println!("\n=== Render Integration Test ===");
println!("Platform: {:?}", detect_platform());
let object_dir = PathBuf::from("/tmp/ycb/003_cracker_box");
if !object_dir.exists() {
println!("⚠ YCB models not found at {:?}", object_dir);
println!(" Skipping render test (models required)");
println!(" Run: cargo run --example test_render");
return;
}
let viewpoint_config = ViewpointConfig::default();
let viewpoints = bevy_sensor::generate_viewpoints(&viewpoint_config);
assert!(!viewpoints.is_empty(), "No viewpoints generated");
let config = RenderConfig::tbp_default();
println!("Rendering {}x{}", config.width, config.height);
let output = render_to_buffer(
&object_dir,
&viewpoints[0],
&ObjectRotation::identity(),
&config,
)
.expect("Render failed - GPU/backend unavailable or rendering not supported");
assert_eq!(output.width, config.width, "Output width mismatch");
assert_eq!(output.height, config.height, "Output height mismatch");
let expected_rgba_size = (config.width * config.height * 4) as usize;
assert_eq!(
output.rgba.len(),
expected_rgba_size,
"RGBA buffer size mismatch: expected {} bytes, got {}",
expected_rgba_size,
output.rgba.len()
);
let expected_depth_size = (config.width * config.height) as usize;
assert_eq!(
output.depth.len(),
expected_depth_size,
"Depth buffer size mismatch: expected {} values, got {}",
expected_depth_size,
output.depth.len()
);
let mut has_valid_depth = false;
for &depth in output.depth.iter() {
if depth > 0.1 && depth < 10.0 {
has_valid_depth = true;
break;
}
}
assert!(has_valid_depth, "No valid depth values in output");
let mut has_color = false;
for chunk in output.rgba.chunks(4) {
if chunk[0] > 10 || chunk[1] > 10 || chunk[2] > 10 {
has_color = true;
break;
}
}
assert!(has_color, "No color data in output");
let intrinsics = &output.intrinsics;
assert!(intrinsics.focal_length[0] > 0.0, "Invalid focal length X");
assert!(intrinsics.focal_length[1] > 0.0, "Invalid focal length Y");
assert!(
intrinsics.principal_point[0] >= 0.0,
"Invalid principal point X"
);
assert!(
intrinsics.principal_point[1] >= 0.0,
"Invalid principal point Y"
);
println!("✓ Render output valid!");
println!(" RGBA: {} bytes", output.rgba.len());
println!(
" Depth: {} values ({} bytes)",
output.depth.len(),
output.depth.len() * 8
);
println!(
" Focal length: [{:.2}, {:.2}]",
intrinsics.focal_length[0], intrinsics.focal_length[1]
);
if let Err(e) = save_render_output(&output, "test_render_basic") {
println!("⚠ Failed to save render output: {}", e);
}
println!("✓ Integration test passed");
}
#[test]
#[ignore] fn test_batch_render_matches_sequential_episode_outputs() {
println!("\n=== Batch vs Sequential Render Test ===");
let object_dir = PathBuf::from("/tmp/ycb/003_cracker_box");
if !object_dir.exists() {
println!("⚠ Skipping - YCB models not found");
return;
}
let viewpoint_config = ViewpointConfig::default();
let viewpoints = bevy_sensor::generate_viewpoints(&viewpoint_config);
let selected_viewpoints: Vec<_> = viewpoints.into_iter().take(3).collect();
let rotation = ObjectRotation::identity();
let config = RenderConfig::tbp_default();
let sequential_start = Instant::now();
let sequential_outputs: Vec<_> = selected_viewpoints
.iter()
.map(|viewpoint| {
render_to_buffer(&object_dir, viewpoint, &rotation, &config)
.expect("Sequential render failed")
})
.collect();
let sequential_elapsed = sequential_start.elapsed();
let batch_requests: Vec<_> = selected_viewpoints
.iter()
.map(|viewpoint| BatchRenderRequest {
object_dir: object_dir.clone(),
viewpoint: *viewpoint,
object_rotation: rotation.clone(),
render_config: config.clone(),
})
.collect();
let batch_start = Instant::now();
let batch_outputs =
render_batch(batch_requests, &BatchRenderConfig::default()).expect("Batch render failed");
let batch_elapsed = batch_start.elapsed();
assert_eq!(batch_outputs.len(), sequential_outputs.len());
for (idx, (batch_output, sequential_output)) in batch_outputs
.iter()
.zip(sequential_outputs.iter())
.enumerate()
{
assert_eq!(batch_output.request.viewpoint, selected_viewpoints[idx]);
assert_eq!(batch_output.request.object_rotation, rotation);
assert_eq!(batch_output.width, sequential_output.width);
assert_eq!(batch_output.height, sequential_output.height);
assert_eq!(batch_output.intrinsics, sequential_output.intrinsics);
assert_eq!(batch_output.rgba, sequential_output.rgba);
assert_eq!(batch_output.depth.len(), sequential_output.depth.len());
let max_depth_delta = batch_output
.depth
.iter()
.zip(sequential_output.depth.iter())
.map(|(lhs, rhs)| (lhs - rhs).abs())
.fold(0.0_f64, f64::max);
assert!(
max_depth_delta <= 1e-9,
"Depth mismatch at viewpoint {idx}: max delta {max_depth_delta}"
);
}
println!(
" Sequential: {:.2}s for {} viewpoints",
sequential_elapsed.as_secs_f64(),
sequential_outputs.len()
);
println!(
" Batch: {:.2}s for {} viewpoints",
batch_elapsed.as_secs_f64(),
batch_outputs.len()
);
println!("✓ Batch and sequential outputs matched");
}
#[test]
#[ignore] fn test_render_multiple_viewpoints() {
println!("\n=== Multiple Viewpoint Render Test ===");
let object_dir = PathBuf::from("/tmp/ycb/003_cracker_box");
if !object_dir.exists() {
println!("⚠ Skipping - YCB models not found");
return;
}
let viewpoint_config = ViewpointConfig::default();
let viewpoints = bevy_sensor::generate_viewpoints(&viewpoint_config);
let config = RenderConfig::tbp_default();
println!("Rendering {} viewpoints...", viewpoints.len().min(3));
for (i, viewpoint) in viewpoints.iter().take(3).enumerate() {
let output = render_to_buffer(&object_dir, viewpoint, &ObjectRotation::identity(), &config)
.expect("Render failed");
assert_eq!(output.width, config.width);
assert_eq!(output.height, config.height);
assert_eq!(
output.rgba.len(),
(config.width * config.height * 4) as usize
);
assert_eq!(output.depth.len(), (config.width * config.height) as usize);
if let Err(e) = save_render_output(&output, &format!("test_viewpoint_{}", i)) {
println!(" ⚠ Failed to save: {}", e);
}
println!(" ✓ Viewpoint {} rendered successfully", i);
}
println!("✓ Multiple viewpoint test passed");
}
#[test]
#[ignore] fn test_render_with_rotation() {
println!("\n=== Render with Object Rotation Test ===");
let object_dir = PathBuf::from("/tmp/ycb/003_cracker_box");
if !object_dir.exists() {
println!("⚠ Skipping - YCB models not found");
return;
}
let viewpoint_config = ViewpointConfig::default();
let viewpoints = bevy_sensor::generate_viewpoints(&viewpoint_config);
let config = RenderConfig::tbp_default();
let rotations = ObjectRotation::tbp_benchmark_rotations();
println!("Rendering with {} rotations...", rotations.len());
for (rot_idx, rotation) in rotations.iter().enumerate() {
let output = render_to_buffer(&object_dir, &viewpoints[0], rotation, &config)
.expect("Render with rotation failed");
assert_eq!(output.width, config.width);
assert_eq!(output.height, config.height);
if let Err(e) = save_render_output(&output, &format!("test_rotation_{}", rot_idx)) {
println!(" ⚠ Failed to save: {}", e);
}
println!(
" ✓ Rotation {} rendered successfully (yaw: {}°)",
rot_idx, rotation.yaw
);
}
println!("✓ Rotation test passed");
}
#[test]
#[ignore] fn test_render_with_cache() {
println!("\n=== Render with Cache Test ===");
let object_dir = PathBuf::from("/tmp/ycb/003_cracker_box");
if !object_dir.exists() {
println!("⚠ Skipping - YCB models not found");
return;
}
let viewpoint_config = ViewpointConfig::default();
let viewpoints = bevy_sensor::generate_viewpoints(&viewpoint_config);
let config = RenderConfig::tbp_default();
let mut cache = ModelCache::new();
let output1 = render_to_buffer_cached(
&object_dir,
&viewpoints[0],
&ObjectRotation::identity(),
&config,
&mut cache,
)
.expect("First cached render failed");
println!(" ✓ First render completed");
println!(
" Cache stats: {} scenes, {} textures",
cache.scene_count(),
cache.texture_count()
);
assert_eq!(cache.scene_count(), 1, "Should have 1 cached scene");
assert_eq!(cache.texture_count(), 1, "Should have 1 cached texture");
let output2 = render_to_buffer_cached(
&object_dir,
&viewpoints[1],
&ObjectRotation::identity(),
&config,
&mut cache,
)
.expect("Second cached render failed");
println!(" ✓ Second render completed");
println!(
" Cache stats: {} scenes, {} textures",
cache.scene_count(),
cache.texture_count()
);
assert_eq!(cache.scene_count(), 1, "Should still have 1 cached scene");
assert_eq!(
cache.texture_count(),
1,
"Should still have 1 cached texture"
);
assert_eq!(output1.width, config.width);
assert_eq!(output1.height, config.height);
assert_eq!(output2.width, config.width);
assert_eq!(output2.height, config.height);
assert_ne!(
output1.rgba, output2.rgba,
"Different viewpoints should produce different RGBA"
);
if let Err(e) = save_render_output(&output1, "test_cache_vp0") {
println!(" ⚠ Failed to save output1: {}", e);
}
if let Err(e) = save_render_output(&output2, "test_cache_vp1") {
println!(" ⚠ Failed to save output2: {}", e);
}
cache.clear();
assert_eq!(cache.scene_count(), 0, "Cache should be empty after clear");
assert_eq!(
cache.texture_count(),
0,
"Cache should be empty after clear"
);
println!("✓ Cache test passed");
}
#[test]
#[ignore] fn test_cache_with_multiple_viewpoints() {
println!("\n=== Cache with Multiple Viewpoints Test ===");
let object_dir = PathBuf::from("/tmp/ycb/005_tomato_soup_can");
if !object_dir.exists() {
println!("⚠ Skipping - YCB model not found");
return;
}
let viewpoint_config = ViewpointConfig::default();
let viewpoints = bevy_sensor::generate_viewpoints(&viewpoint_config);
let config = RenderConfig::tbp_default();
let mut cache = ModelCache::new();
let render_count = 5.min(viewpoints.len());
println!("Rendering {} viewpoints with cache...", render_count);
let mut outputs = Vec::new();
for (i, viewpoint) in viewpoints.iter().take(render_count).enumerate() {
let output = render_to_buffer_cached(
&object_dir,
viewpoint,
&ObjectRotation::identity(),
&config,
&mut cache,
)
.expect("Cached render failed");
outputs.push(output);
if i == 0 {
println!(
" Initial cache size: {} scenes, {} textures",
cache.scene_count(),
cache.texture_count()
);
}
}
assert_eq!(cache.scene_count(), 1, "Should cache 1 scene");
assert_eq!(cache.texture_count(), 1, "Should cache 1 texture");
for (i, output) in outputs.iter().enumerate() {
assert_eq!(output.width, config.width, "Output {} width mismatch", i);
assert_eq!(output.height, config.height, "Output {} height mismatch", i);
assert!(!output.rgba.is_empty(), "Output {} has no RGBA data", i);
assert!(!output.depth.is_empty(), "Output {} has no depth data", i);
}
println!(" ✓ All {} renders successful", render_count);
println!(
" ✓ Cache maintained 1 scene and 1 texture across {} viewpoints",
render_count
);
println!("✓ Multiple viewpoints cache test passed");
}