From d7aeb06548934d8b53bd704dd8e7fdd0354a4fc8 Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 11 Apr 2026 10:32:43 -0400 Subject: [PATCH] perf: binary search event selection with pre-sorted index --- .../src/renderer/event_selector.rs | 109 ++++++++++++------ 1 file changed, 76 insertions(+), 33 deletions(-) diff --git a/crates/ass-renderer/src/renderer/event_selector.rs b/crates/ass-renderer/src/renderer/event_selector.rs index b9fa719..60cdcfa 100644 --- a/crates/ass-renderer/src/renderer/event_selector.rs +++ b/crates/ass-renderer/src/renderer/event_selector.rs @@ -8,6 +8,17 @@ use alloc::{collections::BTreeSet, vec::Vec}; #[cfg(not(feature = "nostd"))] use std::collections::HashSet; +/// Pre-sorted event entry for binary search selection. +#[derive(Debug, Clone)] +struct SortedEvent { + /// Index into the events section + index: usize, + /// Start time in centiseconds + start_cs: u32, + /// End time in centiseconds + end_cs: u32, +} + /// Tracks active events and dirty regions for optimized rendering #[derive(Debug, Clone)] pub struct EventSelector { @@ -25,6 +36,14 @@ pub struct EventSelector { /// Whether to render comment events (for signs and complex effects) render_comments: bool, + + /// Pre-sorted event index, built lazily on first select_active call. + /// Sorted by start_cs for binary search via partition_point. + sorted_events: Vec, + + /// Number of events when the sorted index was last built. + /// Triggers a rebuild if the event count changes. + sorted_event_count: usize, } /// A region that needs re-rendering @@ -64,6 +83,8 @@ impl EventSelector { last_timestamp: None, dirty_regions: Vec::new(), render_comments: true, // Enable by default for compatibility with sign rendering + sorted_events: Vec::new(), + sorted_event_count: 0, } } @@ -72,7 +93,36 @@ impl EventSelector { self.render_comments = render; } - /// Select active events and track changes for incremental rendering + /// Build or rebuild the sorted event index from the events section. + fn build_sorted_index(&mut self, events: &[Event<'_>]) { + self.sorted_events.clear(); + self.sorted_events.reserve(events.len()); + + for (idx, event) in events.iter().enumerate() { + let should_include = match event.event_type { + EventType::Dialogue => true, + EventType::Comment => self.render_comments, + _ => false, + }; + + if should_include { + self.sorted_events.push(SortedEvent { + index: idx, + start_cs: event.start_time_cs().unwrap_or(0), + end_cs: event.end_time_cs().unwrap_or(0), + }); + } + } + + // Sort by start_cs for binary search + self.sorted_events.sort_by_key(|e| e.start_cs); + self.sorted_event_count = events.len(); + } + + /// Select active events and track changes for incremental rendering. + /// + /// Uses a pre-sorted index with binary search (partition_point) for + /// O(log n + k) typical case, where k is the number of active events. pub fn select_active<'a>( &mut self, script: &'a Script<'a>, @@ -84,7 +134,7 @@ impl EventSelector { #[cfg(feature = "nostd")] let mut current_active = BTreeSet::new(); - // Find all active events + // Find all active events using binary search on sorted index if let Some(events_section) = script.sections().iter().find_map(|section| { if let Section::Events(events) = section { Some(events) @@ -92,35 +142,36 @@ impl EventSelector { None } }) { + // Build or rebuild sorted index if needed + if self.sorted_event_count != events_section.len() { + self.build_sorted_index(events_section); + } + log::trace!( - "EventSelector: Checking {} events at time {}", + "EventSelector: Checking {} events at time {} (sorted index: {} entries)", events_section.len(), - time_cs + time_cs, + self.sorted_events.len(), ); - for (idx, event) in events_section.iter().enumerate() { - // Include both Dialogue and optionally Comment events - let should_include = match event.event_type { - EventType::Dialogue => true, - EventType::Comment => self.render_comments, - _ => false, - }; - - if should_include { - let start = event.start_time_cs().unwrap_or(0); - let end = event.end_time_cs().unwrap_or(0); + + // Binary search: find the partition point where start_cs > time_cs. + // All events with start_cs <= time_cs are in sorted_events[..partition]. + let partition = self.sorted_events.partition_point(|e| e.start_cs <= time_cs); + + for se in &self.sorted_events[..partition] { + if se.end_cs >= time_cs { + let event = &events_section[se.index]; // Debug output for fade events if event.text.contains("\\fad") { - log::trace!("EventSelector: Fade event - start={}cs, end={}cs, time_cs={}cs, active={}", - start, end, time_cs, start <= time_cs && end >= time_cs); + log::trace!( + "EventSelector: Fade event - start={}cs, end={}cs, time_cs={}cs, active=true", + se.start_cs, se.end_cs, time_cs + ); } - // Events are active from start time (inclusive) to end time (inclusive) - // This matches ASS/SSA specification and libass behavior - if start <= time_cs && end >= time_cs { - active_events.push(event); - current_active.insert(idx); - } + active_events.push(event); + current_active.insert(se.index); } } } @@ -232,6 +283,8 @@ impl EventSelector { self.previous_active.clear(); self.last_timestamp = None; self.dirty_regions.clear(); + self.sorted_events.clear(); + self.sorted_event_count = 0; } } @@ -240,13 +293,3 @@ impl Default for EventSelector { Self::new() } } - -/// Legacy function for backward compatibility -#[allow(dead_code)] // Kept for backward compatibility -pub fn select_active_events<'a>(script: &'a Script<'a>, time_cs: u32) -> Vec<&'a Event<'a>> { - let mut selector = EventSelector::new(); - selector - .select_active(script, time_cs) - .map(|active| active.events) - .unwrap_or_default() -} -- 2.51.2