Skip to main content

ass_renderer/pipeline/build/
run.rs

1//! `Pipeline` trait implementation and per-event processing for the software pipeline.
2
3#[cfg(feature = "nostd")]
4use alloc::{string::ToString, vec::Vec};
5#[cfg(not(feature = "nostd"))]
6use std::{string::ToString, vec::Vec};
7
8#[cfg(feature = "analysis-integration")]
9use ass_core::analysis::ScriptAnalysis;
10use ass_core::parser::{Event, Script};
11
12use super::OwnedStyle;
13use crate::collision::PositionedEvent;
14use crate::pipeline::{text_segmenter::segment_text_with_tags, IntermediateLayer, Pipeline};
15use crate::renderer::RenderContext;
16use crate::utils::{DirtyRegion, RenderError};
17
18impl super::SoftwarePipeline {
19    fn process_event(
20        &mut self,
21        event: &Event,
22        time_cs: u32,
23        context: &RenderContext,
24    ) -> Result<Vec<IntermediateLayer>, RenderError> {
25        // Get text segments with their individual tags
26        let segments = segment_text_with_tags(event.text, None)?;
27
28        if segments.is_empty() {
29            return Ok(Vec::new());
30        }
31
32        // Check if this is a drawing command
33        if let Some(draw_level) = segments[0].tags.drawing_mode {
34            if draw_level > 0 {
35                // Clone the style to avoid borrow issues
36                let style_cloned = self
37                    .styles_map
38                    .get(event.style)
39                    .or(self.default_style.as_ref())
40                    .cloned();
41
42                return self.process_drawing_command(
43                    &segments[0],
44                    event,
45                    style_cloned.as_ref(),
46                    time_cs,
47                    context,
48                );
49            }
50        }
51
52        // Clone the style to avoid borrow issues
53        let style_cloned = self
54            .styles_map
55            .get(event.style)
56            .or(self.default_style.as_ref())
57            .cloned();
58
59        // Process text segments with proper style inheritance
60        self.process_text_segments(segments, event, style_cloned.as_ref(), time_cs, context)
61    }
62}
63
64#[allow(dead_code)] // Utility for karaoke effects - used in future features
65fn calculate_karaoke_progress(time_cs: u32, start_time_cs: u32, duration_cs: u32) -> f32 {
66    if time_cs < start_time_cs {
67        return 0.0;
68    }
69    let elapsed = time_cs - start_time_cs;
70    if elapsed >= duration_cs {
71        return 1.0;
72    }
73    elapsed as f32 / duration_cs as f32
74}
75
76impl Pipeline for super::SoftwarePipeline {
77    fn prepare_script(
78        &mut self,
79        script: &Script,
80        #[cfg(feature = "analysis-integration")] analysis: Option<&ScriptAnalysis>,
81        #[cfg(not(feature = "analysis-integration"))] _analysis: Option<()>,
82    ) -> Result<(), RenderError> {
83        // Load embedded and referenced fonts from the script
84        super::super::font_loader::load_script_fonts(script, &mut self.font_database);
85
86        // Clear and rebuild styles map
87        self.styles_map.clear();
88        self.default_style = None;
89
90        // If we have analysis with resolved styles (which handle LayoutRes->PlayRes scaling),
91        // we should use those instead of raw styles
92        #[cfg(feature = "analysis-integration")]
93        let _use_resolved_styles = analysis.is_some();
94        #[cfg(not(feature = "analysis-integration"))]
95        let _use_resolved_styles = false;
96
97        // Extract script info and styles from the script
98        for section in script.sections() {
99            match section {
100                ass_core::parser::Section::ScriptInfo(info) => {
101                    // Extract PlayResX and PlayResY from script info
102                    if let Some((res_x, res_y)) = info.play_resolution() {
103                        self.play_res_x = res_x as f32;
104                        self.play_res_y = res_y as f32;
105                    }
106
107                    // Extract WrapStyle (0 smart / 1 greedy / 2 none / 3 smart)
108                    self.wrap_style = info.wrap_style();
109
110                    // Extract LayoutResX and LayoutResY if present
111                    if let Some((layout_x, layout_y)) = info.layout_resolution() {
112                        self.layout_res_x = Some(layout_x as f32);
113                        self.layout_res_y = Some(layout_y as f32);
114
115                        // If LayoutRes differs from PlayRes, we need to scale styles
116                        // This is done later when processing styles
117                    }
118
119                    // Extract ScaledBorderAndShadow setting
120                    // Default is "yes" per ASS spec, but can be "no" to disable scaling
121                    if let Some(scaled_value) = info.get_field("ScaledBorderAndShadow") {
122                        self.scaled_border_and_shadow = scaled_value.to_lowercase() != "no";
123                    }
124                }
125                ass_core::parser::Section::Styles(styles) => {
126                    // Calculate LayoutRes->PlayRes scaling factors if LayoutRes is present
127                    let layout_to_play_scale_x = if let Some(layout_x) = self.layout_res_x {
128                        if layout_x != self.play_res_x {
129                            self.play_res_x / layout_x
130                        } else {
131                            1.0
132                        }
133                    } else {
134                        1.0
135                    };
136
137                    let layout_to_play_scale_y = if let Some(layout_y) = self.layout_res_y {
138                        if layout_y != self.play_res_y {
139                            self.play_res_y / layout_y
140                        } else {
141                            1.0
142                        }
143                    } else {
144                        1.0
145                    };
146
147                    let needs_layout_scaling =
148                        layout_to_play_scale_x != 1.0 || layout_to_play_scale_y != 1.0;
149
150                    for style in styles {
151                        let style_name = style.name.to_string();
152                        let mut owned_style = OwnedStyle::from_style(style);
153
154                        // Apply LayoutRes->PlayRes scaling if needed
155                        if needs_layout_scaling {
156                            // Scale font size (using Y scale as per libass)
157                            if let Ok(font_size) = owned_style.fontsize.parse::<f32>() {
158                                owned_style.fontsize =
159                                    (font_size * layout_to_play_scale_y).to_string();
160                            }
161
162                            // Scale margins
163                            if let Ok(margin_l) = owned_style.margin_l.parse::<f32>() {
164                                owned_style.margin_l =
165                                    (margin_l * layout_to_play_scale_x).to_string();
166                            }
167                            if let Ok(margin_r) = owned_style.margin_r.parse::<f32>() {
168                                owned_style.margin_r =
169                                    (margin_r * layout_to_play_scale_x).to_string();
170                            }
171                            if let Ok(margin_v) = owned_style.margin_v.parse::<f32>() {
172                                owned_style.margin_v =
173                                    (margin_v * layout_to_play_scale_y).to_string();
174                            }
175
176                            // Scale outline and shadow if ScaledBorderAndShadow is enabled
177                            if self.scaled_border_and_shadow {
178                                if let Ok(outline) = owned_style.outline.parse::<f32>() {
179                                    owned_style.outline =
180                                        (outline * layout_to_play_scale_y).to_string();
181                                }
182                                if let Ok(shadow) = owned_style.shadow.parse::<f32>() {
183                                    owned_style.shadow =
184                                        (shadow * layout_to_play_scale_y).to_string();
185                                }
186                            }
187
188                            // Scale spacing
189                            if let Ok(spacing) = owned_style.spacing.parse::<f32>() {
190                                owned_style.spacing =
191                                    (spacing * layout_to_play_scale_x).to_string();
192                            }
193                        }
194
195                        if style_name == "Default" || style_name == "*Default" {
196                            self.default_style = Some(owned_style.clone());
197                        }
198
199                        self.styles_map.insert(style_name, owned_style);
200                    }
201                }
202                _ => {}
203            }
204        }
205
206        // If no default style found, use the first one
207        if self.default_style.is_none() && !self.styles_map.is_empty() {
208            self.default_style = self.styles_map.values().next().cloned();
209        }
210
211        Ok(())
212    }
213
214    fn script(&self) -> Option<&Script<'_>> {
215        None // We don't store the script reference directly
216    }
217
218    fn process_events(
219        &mut self,
220        events: &[&Event],
221        time_cs: u32,
222        context: &RenderContext,
223    ) -> Result<Vec<IntermediateLayer>, RenderError> {
224        // Clear collision resolver for this frame (but keep dimensions)
225        self.collision_resolver.clear();
226
227        // Pre-allocate with estimated capacity to reduce allocations
228        let mut all_layers = Vec::with_capacity(events.len() * 3);
229
230        // Sort events first by layer, then by start time to ensure proper ordering
231        let mut sorted_events = events.to_vec();
232        sorted_events.sort_by(|a, b| {
233            let layer_a = a.layer.parse::<i32>().unwrap_or(0);
234            let layer_b = b.layer.parse::<i32>().unwrap_or(0);
235            let start_a = a.start_time_cs().unwrap_or(0);
236            let start_b = b.start_time_cs().unwrap_or(0);
237
238            // Sort by layer first, then by start time
239            layer_a.cmp(&layer_b).then(start_a.cmp(&start_b))
240        });
241
242        let scale_y = context.height() as f32 / self.play_res_y;
243
244        // Process each event, applying collision resolution so simultaneous
245        // non-positioned events stack instead of overlapping (libass "Normal"
246        // collisions). Positioned events (\pos/\move) are exempt and do not
247        // participate in stacking.
248        for event in sorted_events {
249            let mut event_layers = self.process_event(event, time_cs, context)?;
250
251            if !Self::event_is_positioned(event) {
252                if let Some(bbox) = self.event_bounding_box(&event_layers) {
253                    let positioned = PositionedEvent {
254                        bbox,
255                        layer: event.layer.parse::<i32>().unwrap_or(0),
256                        margin_v: self.event_margin_v(event, scale_y) as i32,
257                        margin_l: 0,
258                        margin_r: 0,
259                        alignment: self.effective_alignment(event),
260                        priority: 0,
261                    };
262                    let resolved = self.collision_resolver.find_position(positioned);
263                    let dy = resolved.y - bbox.y;
264                    if dy.abs() > 0.5 {
265                        Self::offset_layers_y(&mut event_layers, dy);
266                    }
267                }
268            }
269
270            all_layers.extend(event_layers);
271        }
272
273        Ok(all_layers)
274    }
275
276    fn compute_dirty_regions(
277        &self,
278        events: &[&Event],
279        time_cs: u32,
280        prev_time_cs: u32,
281    ) -> Result<Vec<DirtyRegion>, RenderError> {
282        let mut regions = Vec::new();
283
284        for event in events {
285            let was_active = event.start_time_cs().unwrap_or(0) <= prev_time_cs
286                && event.end_time_cs().unwrap_or(u32::MAX) > prev_time_cs;
287            let is_active = event.start_time_cs().unwrap_or(0) <= time_cs
288                && event.end_time_cs().unwrap_or(u32::MAX) > time_cs;
289
290            if was_active != is_active {
291                // Event visibility changed, mark entire screen as dirty for now
292                regions.push(DirtyRegion::full_screen());
293                break;
294            }
295        }
296
297        Ok(regions)
298    }
299}