Skip to main content

ass_renderer/renderer/
mod.rs

1//! Core renderer implementation
2
3use crate::backends::RenderBackend;
4use crate::pipeline::Pipeline;
5use crate::utils::RenderError;
6use ass_core::parser::Script;
7
8#[cfg(feature = "nostd")]
9use alloc::{boxed::Box, vec::Vec};
10#[cfg(not(feature = "nostd"))]
11use std::boxed::Box;
12
13mod context;
14mod event_selector;
15mod frame;
16mod metrics;
17mod probing;
18mod time_index;
19
20pub use context::RenderContext;
21pub use event_selector::{ActiveEvents, DirtyRegion, EventSelector};
22pub use frame::Frame;
23pub use metrics::{CacheStatistics, PerformanceMetrics};
24pub use probing::BackendProber;
25
26/// Main renderer that coordinates rendering pipeline
27pub struct Renderer {
28    context: RenderContext,
29    backend: Box<dyn RenderBackend>,
30    pipeline: Box<dyn Pipeline>,
31    event_selector: event_selector::EventSelector,
32    /// Cache of the last fully-static rendered frame, keyed by the active events'
33    /// text spans. Reused (data copied, timestamp updated) when no active event is
34    /// animated and the active set is unchanged — the common case of a subtitle
35    /// shown across many frames. Animated frames (`\t`/`\move`/`\k`/`\fad`) skip it.
36    frame_cache: Option<(Vec<(usize, usize)>, Frame)>,
37}
38
39impl Renderer {
40    /// Create a new renderer with the given backend type and context
41    pub fn new(
42        backend_type: crate::backends::BackendType,
43        context: RenderContext,
44    ) -> Result<Self, RenderError> {
45        let backend =
46            crate::backends::create_backend(backend_type, context.width(), context.height())?;
47        let pipeline = backend.create_pipeline()?;
48
49        Ok(Self {
50            context,
51            backend,
52            pipeline,
53            event_selector: event_selector::EventSelector::new(),
54            frame_cache: None,
55        })
56    }
57
58    /// Create a new renderer with a specific backend instance
59    pub fn with_backend(
60        context: RenderContext,
61        backend: Box<dyn RenderBackend>,
62    ) -> Result<Self, RenderError> {
63        let pipeline = backend.create_pipeline()?;
64
65        Ok(Self {
66            context,
67            backend,
68            pipeline,
69            event_selector: event_selector::EventSelector::new(),
70            frame_cache: None,
71        })
72    }
73
74    /// Create renderer with automatic backend detection
75    #[cfg(feature = "backend-probing")]
76    pub fn with_auto_backend(context: RenderContext) -> Result<Self, RenderError> {
77        let prober = BackendProber::new();
78        let backend = prober.probe_best_backend(&context)?;
79        Self::with_backend(context, backend)
80    }
81
82    /// Render a frame for the given script at the specified time
83    pub fn render_frame(&mut self, script: &Script, time_cs: u32) -> Result<Frame, RenderError> {
84        // Extract script resolution and update context
85        for section in script.sections() {
86            if let ass_core::parser::Section::ScriptInfo(info) = section {
87                if let Some((play_x, play_y)) = info.play_resolution() {
88                    self.context.set_playback_resolution(play_x, play_y);
89                }
90                if let Some((layout_x, layout_y)) = info.layout_resolution() {
91                    self.context.set_storage_resolution(layout_x, layout_y);
92                }
93                break; // Only need first ScriptInfo section
94            }
95        }
96
97        let active = self.event_selector.select_active(script, time_cs)?;
98        let events = active.events;
99
100        if events.is_empty() {
101            return Ok(Frame::empty(
102                self.context.width(),
103                self.context.height(),
104                time_cs,
105            ));
106        }
107
108        // Frame cache: when no active event is animated, the rendered output is
109        // identical for every time the same events are active, so reuse the last
110        // render (copying its pixels with the current timestamp) instead of
111        // re-shaping and re-rasterizing. Animated frames bypass and clear it.
112        let animated = events.iter().any(|e| Self::event_is_animated(e.text));
113        let cache_key: Option<Vec<(usize, usize)>> = (!animated).then(|| {
114            events
115                .iter()
116                .map(|e| (e.text.as_ptr() as usize, e.text.len()))
117                .collect()
118        });
119        if let (Some(key), Some((cached_key, cached))) =
120            (cache_key.as_ref(), self.frame_cache.as_ref())
121        {
122            if cached_key == key {
123                return Ok(cached.with_timestamp(time_cs));
124            }
125        }
126
127        // The pipeline does not consume a ScriptAnalysis, so it is not computed
128        // here — analysing the whole script every frame was pathologically slow on
129        // large files (tens of seconds for a full episode).
130        self.pipeline.prepare_script(script, None)?;
131        let layers = self
132            .pipeline
133            .process_events(&events, time_cs, &self.context)?;
134        let frame_data = self.backend.composite_layers(&layers, &self.context)?;
135
136        let frame = Frame::new(
137            frame_data,
138            self.context.width(),
139            self.context.height(),
140            time_cs,
141        );
142        self.frame_cache = cache_key.map(|key| (key, frame.clone()));
143        Ok(frame)
144    }
145
146    /// Render the active subtitles at `time_cs` to a positioned bitmap list
147    /// (libass `ASS_Image` style) rather than a composited frame.
148    ///
149    /// The caller (typically a video player or GPU) composites the returned
150    /// bitmaps. This skips the renderer's full-frame clear, the final composite
151    /// blend and the frame-buffer copy — the model real integrations use, and the
152    /// apples-to-apples shape of libass's own output. Requires a software backend.
153    #[cfg(feature = "software-backend")]
154    pub fn render_frame_bitmaps(
155        &mut self,
156        script: &Script,
157        time_cs: u32,
158    ) -> Result<Vec<crate::backends::coverage::RenderBitmap>, RenderError> {
159        for section in script.sections() {
160            if let ass_core::parser::Section::ScriptInfo(info) = section {
161                if let Some((play_x, play_y)) = info.play_resolution() {
162                    self.context.set_playback_resolution(play_x, play_y);
163                }
164                if let Some((layout_x, layout_y)) = info.layout_resolution() {
165                    self.context.set_storage_resolution(layout_x, layout_y);
166                }
167                break;
168            }
169        }
170
171        let active = self.event_selector.select_active(script, time_cs)?;
172        let events = active.events;
173        if events.is_empty() {
174            return Ok(Vec::new());
175        }
176
177        self.pipeline.prepare_script(script, None)?;
178        let layers = self
179            .pipeline
180            .process_events(&events, time_cs, &self.context)?;
181        self.backend
182            .render_layers_to_bitmaps(&layers, &self.context)
183    }
184
185    /// Whether an event's text carries a time-dependent override (`\t`, `\move`,
186    /// karaoke `\k`/`\K`, or `\fad`), meaning its output changes between frames
187    /// and must not be served from the static frame cache.
188    fn event_is_animated(text: &str) -> bool {
189        text.contains("\\t")
190            || text.contains("\\move")
191            || text.contains("\\fad")
192            || text.contains("\\k")
193            || text.contains("\\K")
194    }
195
196    /// Render frame incrementally (dirty regions only)
197    pub fn render_frame_incremental(
198        &mut self,
199        script: &Script,
200        time_cs: u32,
201        previous_frame: &Frame,
202    ) -> Result<Frame, RenderError> {
203        let active = self.event_selector.select_active(script, time_cs)?;
204        let events = active.events;
205        let dirty_regions =
206            self.pipeline
207                .compute_dirty_regions(&events, time_cs, previous_frame.timestamp())?;
208
209        if dirty_regions.is_empty() {
210            return Ok(previous_frame.clone());
211        }
212
213        self.pipeline.prepare_script(script, None)?;
214        let layers = self
215            .pipeline
216            .process_events(&events, time_cs, &self.context)?;
217        let frame_data = self.backend.composite_layers_incremental(
218            &layers,
219            &dirty_regions,
220            previous_frame.data(),
221            &self.context,
222        )?;
223
224        Ok(Frame::new(
225            frame_data,
226            self.context.width(),
227            self.context.height(),
228            time_cs,
229        ))
230    }
231
232    /// Get current backend type
233    pub fn backend_type(&self) -> crate::backends::BackendType {
234        self.backend.backend_type()
235    }
236
237    /// Get backend metrics if available
238    #[cfg(feature = "backend-metrics")]
239    pub fn backend_metrics(&self) -> Option<crate::backends::BackendMetrics> {
240        self.backend.metrics()
241    }
242
243    /// Update render context
244    pub fn set_context(&mut self, context: RenderContext) {
245        self.context = context;
246    }
247
248    /// Get render context
249    pub fn context(&self) -> &RenderContext {
250        &self.context
251    }
252
253    /// Get mutable render context
254    pub fn context_mut(&mut self) -> &mut RenderContext {
255        &mut self.context
256    }
257
258    /// Set collision resolver for subtitle positioning
259    pub fn set_collision_resolver(
260        &mut self,
261        _resolver: Box<dyn crate::collision::CollisionDetector>,
262    ) {
263        // TODO: Implement collision resolver integration
264    }
265
266    /// Get performance metrics if available
267    pub fn metrics(&self) -> Option<PerformanceMetrics> {
268        // TODO: Implement metrics collection
269        None
270    }
271
272    /// Get cache statistics
273    pub fn cache_stats(&self) -> CacheStatistics {
274        CacheStatistics {
275            glyph_hits: 0,
276            font_entries: 0,
277        }
278    }
279}