#![allow(missing_docs)]
use crate::{Frame, RenderError};
#[cfg(feature = "nostd")]
extern crate alloc;
#[cfg(feature = "nostd")]
use alloc::collections::BTreeMap as HashMap;
#[cfg(feature = "nostd")]
use alloc::{format, string::String, vec::Vec};
#[cfg(not(feature = "nostd"))]
use std::format;
pub mod analyzer;
pub mod color_diagnostic;
pub mod info;
pub mod inspector;
pub mod player;
pub mod util;
pub mod visual_comparison;
pub mod benchmarking;
#[cfg(feature = "libass-compare")]
pub mod libass;
pub use analyzer::{AnalysisReport, FrameAnalyzer};
pub use benchmarking::{
quick_benchmark, BenchmarkConfig, BenchmarkResult, PerformanceBenchmark, PerformanceMetrics,
};
pub use info::{BoundingBoxInfo, DirtyRegionInfo, FrameComparison, FrameDebugInfo};
pub use inspector::FrameInspector;
pub use player::{DebugPlayer, PlayerFrame};
use util::{calculate_checksum, draw_rectangle, draw_text_overlay, save_frame_as_png};
pub struct DebugRenderer {
inner: crate::Renderer,
frame_history: Vec<FrameDebugInfo>,
enable_visual_overlay: bool,
enable_text_output: bool,
output_dir: Option<String>,
}
impl DebugRenderer {
pub fn new(renderer: crate::Renderer) -> Self {
Self {
inner: renderer,
frame_history: Vec::new(),
enable_visual_overlay: false,
enable_text_output: true,
output_dir: None,
}
}
pub fn enable_visual_overlay(&mut self, enable: bool) {
self.enable_visual_overlay = enable;
}
pub fn enable_text_output(&mut self, enable: bool) {
self.enable_text_output = enable;
}
pub fn set_output_dir(&mut self, dir: &str) {
self.output_dir = Some(dir.to_string());
}
pub fn render_frame_debug(
&mut self,
script: &ass_core::parser::Script,
time_ms: u32,
) -> Result<(Frame, FrameDebugInfo), RenderError> {
let start = std::time::Instant::now();
let frame = self.inner.render_frame(script, time_ms)?;
let render_time = start.elapsed().as_secs_f64() * 1000.0;
let debug_info = self.collect_debug_info(&frame, time_ms, render_time);
self.frame_history.push(debug_info.clone());
if self.enable_text_output {
self.output_text_debug(&debug_info);
}
if let Some(ref dir) = self.output_dir {
self.save_visual_debug(&frame, &debug_info, dir, time_ms)?;
}
Ok((frame, debug_info))
}
fn collect_debug_info(&self, frame: &Frame, time_ms: u32, render_time: f64) -> FrameDebugInfo {
let pixels = frame.pixels();
let mut non_transparent = 0;
let mut min_x = frame.width();
let mut min_y = frame.height();
let mut max_x = 0u32;
let mut max_y = 0u32;
for y in 0..frame.height() {
for x in 0..frame.width() {
let idx = ((y * frame.width() + x) * 4) as usize;
if idx + 3 < pixels.len() && pixels[idx + 3] > 0 {
non_transparent += 1;
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
}
}
let bounding_box = if non_transparent > 0 {
Some(BoundingBoxInfo {
min_x,
min_y,
max_x,
max_y,
})
} else {
None
};
let checksum = calculate_checksum(pixels);
FrameDebugInfo {
timestamp_ms: time_ms,
active_events: 0, dirty_regions: Vec::new(), render_time_ms: render_time,
memory_usage_bytes: pixels.len(),
cache_hits: 0, cache_misses: 0, backend_type: "Software".to_string(), frame_checksum: checksum,
non_transparent_pixels: non_transparent,
bounding_box,
}
}
fn output_text_debug(&self, info: &FrameDebugInfo) {
println!("=== Frame Debug Info ===");
println!("Timestamp: {}ms", info.timestamp_ms);
println!("Render Time: {:.2}ms", info.render_time_ms);
println!("Backend: {}", info.backend_type);
println!("Active Events: {}", info.active_events);
println!("Non-transparent Pixels: {}", info.non_transparent_pixels);
if let Some(ref bbox) = info.bounding_box {
println!(
"Bounding Box: ({}, {}) to ({}, {})",
bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y
);
println!(
" Size: {}x{}",
bbox.max_x - bbox.min_x + 1,
bbox.max_y - bbox.min_y + 1
);
}
println!("Memory: {} KB", info.memory_usage_bytes / 1024);
println!("Checksum: 0x{:016x}", info.frame_checksum);
println!(
"Cache: {} hits, {} misses",
info.cache_hits, info.cache_misses
);
if !info.dirty_regions.is_empty() {
println!("Dirty Regions:");
for region in &info.dirty_regions {
println!(
" - {}x{} at ({}, {}): {}",
region.width, region.height, region.x, region.y, region.reason
);
}
}
println!("========================");
}
fn save_visual_debug(
&self,
frame: &Frame,
info: &FrameDebugInfo,
dir: &str,
time_ms: u32,
) -> Result<(), RenderError> {
#[cfg(not(feature = "nostd"))]
std::fs::create_dir_all(dir)
.map_err(|e| RenderError::BackendError(format!("Failed to create debug dir: {e}")))?;
#[cfg(feature = "nostd")]
return Err(RenderError::BackendError(
"File operations not supported in no_std".into(),
));
if self.enable_visual_overlay {
let debug_frame = self.create_debug_overlay(frame, info)?;
save_frame_as_png(&debug_frame, &format!("{dir}/frame_{time_ms:06}_debug.png"))?;
} else {
save_frame_as_png(frame, &format!("{dir}/frame_{time_ms:06}.png"))?;
}
#[cfg(all(not(feature = "nostd"), feature = "serde"))]
{
let json_path = format!("{dir}/frame_{time_ms:06}_info.json");
let json = serde_json::to_string_pretty(&info).map_err(|e| {
RenderError::BackendError(format!("Failed to serialize debug info: {e}"))
})?;
std::fs::write(json_path, json).map_err(|e| {
RenderError::BackendError(format!("Failed to write debug info: {e}"))
})?;
}
Ok(())
}
fn create_debug_overlay(
&self,
frame: &Frame,
info: &FrameDebugInfo,
) -> Result<Frame, RenderError> {
let mut debug_frame = frame.clone();
if let Some(ref bbox) = info.bounding_box {
draw_rectangle(
&mut debug_frame,
bbox.min_x,
bbox.min_y,
bbox.max_x - bbox.min_x + 1,
bbox.max_y - bbox.min_y + 1,
[255, 0, 0, 128],
)?;
}
for region in &info.dirty_regions {
draw_rectangle(
&mut debug_frame,
region.x,
region.y,
region.width,
region.height,
[0, 255, 0, 128],
)?;
}
draw_text_overlay(
&mut debug_frame,
&format!(
"Time: {}ms | Events: {} | Render: {:.1}ms",
info.timestamp_ms, info.active_events, info.render_time_ms
),
10,
10,
)?;
Ok(debug_frame)
}
pub fn get_frame_history(&self) -> &[FrameDebugInfo] {
&self.frame_history
}
pub fn clear_history(&mut self) {
self.frame_history.clear();
}
pub fn compare_frames(&self, idx1: usize, idx2: usize) -> Option<FrameComparison> {
if idx1 >= self.frame_history.len() || idx2 >= self.frame_history.len() {
return None;
}
let frame1 = &self.frame_history[idx1];
let frame2 = &self.frame_history[idx2];
Some(FrameComparison {
checksum_match: frame1.frame_checksum == frame2.frame_checksum,
pixel_diff: (frame1.non_transparent_pixels as i32
- frame2.non_transparent_pixels as i32)
.unsigned_abs(),
render_time_diff: frame1.render_time_ms - frame2.render_time_ms,
bbox_changed: frame1.bounding_box != frame2.bounding_box,
})
}
}