Skip to main content

ass_renderer/debug/
mod.rs

1//! Debug and analysis tools for ASS subtitle rendering
2//!
3//! This module provides comprehensive debugging capabilities including:
4//! - Frame analysis and benchmarking
5//! - Visual comparison tools
6//! - Performance profiling
7
8#![allow(missing_docs)] // Debug module with many internal structures
9
10use 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
21/// Frame analysis and reporting tools
22pub mod analyzer;
23/// Color diagnostic utilities
24pub mod color_diagnostic;
25/// Debug information data structures
26pub mod info;
27/// Frame inspection tools
28pub mod inspector;
29/// Debug player for subtitle playback
30pub mod player;
31/// Frame helper utilities for the debug renderer
32pub mod util;
33/// Visual comparison utilities
34pub mod visual_comparison;
35
36/// Performance benchmarking tools
37pub mod benchmarking;
38
39/// libass FFI bridge for A/B comparison (dev-only, requires native libass).
40#[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
53/// Debug renderer that wraps a normal renderer and provides additional debugging info
54pub struct DebugRenderer {
55    /// The wrapped renderer
56    inner: crate::Renderer,
57    /// History of rendered frames with debug info
58    frame_history: Vec<FrameDebugInfo>,
59    /// Whether to add visual debugging overlay
60    enable_visual_overlay: bool,
61    /// Whether to output debug text to console
62    enable_text_output: bool,
63    /// Directory to save debug output files
64    output_dir: Option<String>,
65}
66
67impl DebugRenderer {
68    /// Create a new debug renderer wrapping the given renderer
69    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    /// Enable or disable visual debugging overlay on rendered frames
80    pub fn enable_visual_overlay(&mut self, enable: bool) {
81        self.enable_visual_overlay = enable;
82    }
83
84    /// Enable or disable text debug output to console
85    pub fn enable_text_output(&mut self, enable: bool) {
86        self.enable_text_output = enable;
87    }
88
89    /// Set the directory where debug output files will be saved
90    pub fn set_output_dir(&mut self, dir: &str) {
91        self.output_dir = Some(dir.to_string());
92    }
93
94    /// Render a frame with full debug instrumentation
95    ///
96    /// Returns both the rendered frame and detailed debug information
97    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        // Render the frame
105        let frame = self.inner.render_frame(script, time_ms)?;
106
107        let render_time = start.elapsed().as_secs_f64() * 1000.0;
108
109        // Collect debug info
110        let debug_info = self.collect_debug_info(&frame, time_ms, render_time);
111
112        // Store in history
113        self.frame_history.push(debug_info.clone());
114
115        // Output debug info if enabled
116        if self.enable_text_output {
117            self.output_text_debug(&debug_info);
118        }
119
120        // Save visual debug if output dir is set
121        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        // Analyze pixels
137        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        // Calculate checksum
162        let checksum = calculate_checksum(pixels);
163
164        FrameDebugInfo {
165            timestamp_ms: time_ms,
166            active_events: 0,          // TODO: Get from renderer
167            dirty_regions: Vec::new(), // TODO: Get from renderer
168            render_time_ms: render_time,
169            memory_usage_bytes: pixels.len(),
170            cache_hits: 0,                        // TODO: Get from renderer
171            cache_misses: 0,                      // TODO: Get from renderer
172            backend_type: "Software".to_string(), // TODO: Get from renderer
173            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        // Create directory if it doesn't exist
226        #[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        // Save frame as PNG with debug overlay if enabled
235        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        // Save debug info as JSON
243        #[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        // Draw bounding box
265        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        // Draw dirty regions
277        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 info text overlay
289        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    /// Get the history of rendered frames with debug information
303    pub fn get_frame_history(&self) -> &[FrameDebugInfo] {
304        &self.frame_history
305    }
306
307    /// Clear the frame history
308    pub fn clear_history(&mut self) {
309        self.frame_history.clear();
310    }
311
312    /// Compare two frames from the history by their indices
313    ///
314    /// Returns `None` if either index is out of bounds
315    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}