1#![allow(missing_docs)] use crate::{Frame, RenderError};
11
12#[cfg(feature = "nostd")]
13extern crate alloc;
14#[cfg(feature = "nostd")]
15use alloc::collections::BTreeMap as HashMap;
16#[cfg(feature = "nostd")]
17use alloc::{format, string::String, vec::Vec};
18#[cfg(not(feature = "nostd"))]
19use std::format;
20
21pub mod analyzer;
23pub mod color_diagnostic;
25pub mod info;
27pub mod inspector;
29pub mod player;
31pub mod util;
33pub mod visual_comparison;
35
36pub mod benchmarking;
38
39#[cfg(feature = "libass-compare")]
41pub mod libass;
42
43pub use analyzer::{AnalysisReport, FrameAnalyzer};
44pub use benchmarking::{
45 quick_benchmark, BenchmarkConfig, BenchmarkResult, PerformanceBenchmark, PerformanceMetrics,
46};
47pub use info::{BoundingBoxInfo, DirtyRegionInfo, FrameComparison, FrameDebugInfo};
48pub use inspector::FrameInspector;
49pub use player::{DebugPlayer, PlayerFrame};
50
51use util::{calculate_checksum, draw_rectangle, draw_text_overlay, save_frame_as_png};
52
53pub struct DebugRenderer {
55 inner: crate::Renderer,
57 frame_history: Vec<FrameDebugInfo>,
59 enable_visual_overlay: bool,
61 enable_text_output: bool,
63 output_dir: Option<String>,
65}
66
67impl DebugRenderer {
68 pub fn new(renderer: crate::Renderer) -> Self {
70 Self {
71 inner: renderer,
72 frame_history: Vec::new(),
73 enable_visual_overlay: false,
74 enable_text_output: true,
75 output_dir: None,
76 }
77 }
78
79 pub fn enable_visual_overlay(&mut self, enable: bool) {
81 self.enable_visual_overlay = enable;
82 }
83
84 pub fn enable_text_output(&mut self, enable: bool) {
86 self.enable_text_output = enable;
87 }
88
89 pub fn set_output_dir(&mut self, dir: &str) {
91 self.output_dir = Some(dir.to_string());
92 }
93
94 pub fn render_frame_debug(
98 &mut self,
99 script: &ass_core::parser::Script,
100 time_ms: u32,
101 ) -> Result<(Frame, FrameDebugInfo), RenderError> {
102 let start = std::time::Instant::now();
103
104 let frame = self.inner.render_frame(script, time_ms)?;
106
107 let render_time = start.elapsed().as_secs_f64() * 1000.0;
108
109 let debug_info = self.collect_debug_info(&frame, time_ms, render_time);
111
112 self.frame_history.push(debug_info.clone());
114
115 if self.enable_text_output {
117 self.output_text_debug(&debug_info);
118 }
119
120 if let Some(ref dir) = self.output_dir {
122 self.save_visual_debug(&frame, &debug_info, dir, time_ms)?;
123 }
124
125 Ok((frame, debug_info))
126 }
127
128 fn collect_debug_info(&self, frame: &Frame, time_ms: u32, render_time: f64) -> FrameDebugInfo {
129 let pixels = frame.pixels();
130 let mut non_transparent = 0;
131 let mut min_x = frame.width();
132 let mut min_y = frame.height();
133 let mut max_x = 0u32;
134 let mut max_y = 0u32;
135
136 for y in 0..frame.height() {
138 for x in 0..frame.width() {
139 let idx = ((y * frame.width() + x) * 4) as usize;
140 if idx + 3 < pixels.len() && pixels[idx + 3] > 0 {
141 non_transparent += 1;
142 min_x = min_x.min(x);
143 min_y = min_y.min(y);
144 max_x = max_x.max(x);
145 max_y = max_y.max(y);
146 }
147 }
148 }
149
150 let bounding_box = if non_transparent > 0 {
151 Some(BoundingBoxInfo {
152 min_x,
153 min_y,
154 max_x,
155 max_y,
156 })
157 } else {
158 None
159 };
160
161 let checksum = calculate_checksum(pixels);
163
164 FrameDebugInfo {
165 timestamp_ms: time_ms,
166 active_events: 0, dirty_regions: Vec::new(), render_time_ms: render_time,
169 memory_usage_bytes: pixels.len(),
170 cache_hits: 0, cache_misses: 0, backend_type: "Software".to_string(), frame_checksum: checksum,
174 non_transparent_pixels: non_transparent,
175 bounding_box,
176 }
177 }
178
179 fn output_text_debug(&self, info: &FrameDebugInfo) {
180 println!("=== Frame Debug Info ===");
181 println!("Timestamp: {}ms", info.timestamp_ms);
182 println!("Render Time: {:.2}ms", info.render_time_ms);
183 println!("Backend: {}", info.backend_type);
184 println!("Active Events: {}", info.active_events);
185 println!("Non-transparent Pixels: {}", info.non_transparent_pixels);
186
187 if let Some(ref bbox) = info.bounding_box {
188 println!(
189 "Bounding Box: ({}, {}) to ({}, {})",
190 bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y
191 );
192 println!(
193 " Size: {}x{}",
194 bbox.max_x - bbox.min_x + 1,
195 bbox.max_y - bbox.min_y + 1
196 );
197 }
198
199 println!("Memory: {} KB", info.memory_usage_bytes / 1024);
200 println!("Checksum: 0x{:016x}", info.frame_checksum);
201 println!(
202 "Cache: {} hits, {} misses",
203 info.cache_hits, info.cache_misses
204 );
205
206 if !info.dirty_regions.is_empty() {
207 println!("Dirty Regions:");
208 for region in &info.dirty_regions {
209 println!(
210 " - {}x{} at ({}, {}): {}",
211 region.width, region.height, region.x, region.y, region.reason
212 );
213 }
214 }
215 println!("========================");
216 }
217
218 fn save_visual_debug(
219 &self,
220 frame: &Frame,
221 info: &FrameDebugInfo,
222 dir: &str,
223 time_ms: u32,
224 ) -> Result<(), RenderError> {
225 #[cfg(not(feature = "nostd"))]
227 std::fs::create_dir_all(dir)
228 .map_err(|e| RenderError::BackendError(format!("Failed to create debug dir: {e}")))?;
229 #[cfg(feature = "nostd")]
230 return Err(RenderError::BackendError(
231 "File operations not supported in no_std".into(),
232 ));
233
234 if self.enable_visual_overlay {
236 let debug_frame = self.create_debug_overlay(frame, info)?;
237 save_frame_as_png(&debug_frame, &format!("{dir}/frame_{time_ms:06}_debug.png"))?;
238 } else {
239 save_frame_as_png(frame, &format!("{dir}/frame_{time_ms:06}.png"))?;
240 }
241
242 #[cfg(all(not(feature = "nostd"), feature = "serde"))]
244 {
245 let json_path = format!("{dir}/frame_{time_ms:06}_info.json");
246 let json = serde_json::to_string_pretty(&info).map_err(|e| {
247 RenderError::BackendError(format!("Failed to serialize debug info: {e}"))
248 })?;
249 std::fs::write(json_path, json).map_err(|e| {
250 RenderError::BackendError(format!("Failed to write debug info: {e}"))
251 })?;
252 }
253
254 Ok(())
255 }
256
257 fn create_debug_overlay(
258 &self,
259 frame: &Frame,
260 info: &FrameDebugInfo,
261 ) -> Result<Frame, RenderError> {
262 let mut debug_frame = frame.clone();
263
264 if let Some(ref bbox) = info.bounding_box {
266 draw_rectangle(
267 &mut debug_frame,
268 bbox.min_x,
269 bbox.min_y,
270 bbox.max_x - bbox.min_x + 1,
271 bbox.max_y - bbox.min_y + 1,
272 [255, 0, 0, 128],
273 )?;
274 }
275
276 for region in &info.dirty_regions {
278 draw_rectangle(
279 &mut debug_frame,
280 region.x,
281 region.y,
282 region.width,
283 region.height,
284 [0, 255, 0, 128],
285 )?;
286 }
287
288 draw_text_overlay(
290 &mut debug_frame,
291 &format!(
292 "Time: {}ms | Events: {} | Render: {:.1}ms",
293 info.timestamp_ms, info.active_events, info.render_time_ms
294 ),
295 10,
296 10,
297 )?;
298
299 Ok(debug_frame)
300 }
301
302 pub fn get_frame_history(&self) -> &[FrameDebugInfo] {
304 &self.frame_history
305 }
306
307 pub fn clear_history(&mut self) {
309 self.frame_history.clear();
310 }
311
312 pub fn compare_frames(&self, idx1: usize, idx2: usize) -> Option<FrameComparison> {
316 if idx1 >= self.frame_history.len() || idx2 >= self.frame_history.len() {
317 return None;
318 }
319
320 let frame1 = &self.frame_history[idx1];
321 let frame2 = &self.frame_history[idx2];
322
323 Some(FrameComparison {
324 checksum_match: frame1.frame_checksum == frame2.frame_checksum,
325 pixel_diff: (frame1.non_transparent_pixels as i32
326 - frame2.non_transparent_pixels as i32)
327 .unsigned_abs(),
328 render_time_diff: frame1.render_time_ms - frame2.render_time_ms,
329 bbox_changed: frame1.bounding_box != frame2.bounding_box,
330 })
331 }
332}