Skip to main content

oximedia_codec/rate_control/
quantizer.rs

1// Copyright 2024 OxiMedia Project
2// Licensed under the Apache License, Version 2.0
3
4//! Advanced quantization parameter (QP) selection.
5//!
6//! This module provides sophisticated QP selection algorithms:
7//!
8//! - **Frame-Level QP** - Base QP selection for each frame
9//! - **Block-Level QP** - Adaptive QP for blocks based on content
10//! - **Temporal QP** - QP modulation based on temporal characteristics
11//! - **Psychovisual QP** - Perceptually-optimized QP selection
12//! - **Rate-Distortion QP** - QP selection for optimal R-D trade-off
13//! - **Hierarchical QP** - QP for hierarchical GOP structures
14//!
15//! # Architecture
16//!
17//! ```text
18//! Frame → QP Selector → Base QP → Block Analyzer → Block QPs
19//!    ↓         ↓           ↓            ↓              ↓
20//! Content  Strategy    Frame QP    Adaptation    Final QPs
21//! ```
22
23#![allow(clippy::cast_lossless)]
24#![allow(clippy::cast_precision_loss)]
25#![allow(clippy::cast_possible_truncation)]
26#![allow(clippy::cast_sign_loss)]
27#![allow(clippy::similar_names)]
28#![allow(clippy::too_many_arguments)]
29#![allow(clippy::struct_excessive_bools)]
30#![forbid(unsafe_code)]
31
32use crate::frame::FrameType;
33
34/// QP selection strategy.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum QpStrategy {
37    /// Constant QP for all frames and blocks.
38    Constant,
39    /// Frame type based QP (I/P/B offsets).
40    FrameTypeBased,
41    /// Complexity-adaptive QP.
42    ComplexityAdaptive,
43    /// Psychovisually-optimized QP.
44    Psychovisual,
45    /// Rate-distortion optimized QP.
46    RateDistortionOptimized,
47    /// Hierarchical QP for B-pyramids.
48    Hierarchical,
49}
50
51impl Default for QpStrategy {
52    fn default() -> Self {
53        Self::ComplexityAdaptive
54    }
55}
56
57/// Quantization parameter selector.
58#[derive(Clone, Debug)]
59pub struct QpSelector {
60    /// QP selection strategy.
61    strategy: QpStrategy,
62    /// Base QP value.
63    base_qp: f32,
64    /// Minimum QP allowed.
65    min_qp: u8,
66    /// Maximum QP allowed.
67    max_qp: u8,
68    /// I-frame QP offset.
69    i_qp_offset: i8,
70    /// P-frame QP offset.
71    p_qp_offset: i8,
72    /// B-frame QP offset.
73    b_qp_offset: i8,
74    /// Enable adaptive quantization.
75    enable_aq: bool,
76    /// AQ strength (0.0-2.0).
77    aq_strength: f32,
78    /// Enable psychovisual optimization.
79    enable_psy: bool,
80    /// Psychovisual strength (0.0-2.0).
81    psy_strength: f32,
82    /// QP adaptation speed (0.0-1.0).
83    adaptation_speed: f32,
84    /// Historical average QP.
85    historical_qp: Vec<f32>,
86    /// Maximum history size.
87    max_history: usize,
88    /// Frame counter.
89    frame_count: u64,
90}
91
92impl QpSelector {
93    /// Create a new QP selector.
94    #[must_use]
95    pub fn new(base_qp: f32, min_qp: u8, max_qp: u8) -> Self {
96        Self {
97            strategy: QpStrategy::default(),
98            base_qp: base_qp.clamp(min_qp as f32, max_qp as f32),
99            min_qp,
100            max_qp,
101            i_qp_offset: -2,
102            p_qp_offset: 0,
103            b_qp_offset: 2,
104            enable_aq: true,
105            aq_strength: 1.0,
106            enable_psy: true,
107            psy_strength: 1.0,
108            adaptation_speed: 0.5,
109            historical_qp: Vec::new(),
110            max_history: 100,
111            frame_count: 0,
112        }
113    }
114
115    /// Set QP selection strategy.
116    pub fn set_strategy(&mut self, strategy: QpStrategy) {
117        self.strategy = strategy;
118    }
119
120    /// Set base QP.
121    pub fn set_base_qp(&mut self, qp: f32) {
122        self.base_qp = qp.clamp(self.min_qp as f32, self.max_qp as f32);
123    }
124
125    /// Set I-frame QP offset.
126    pub fn set_i_qp_offset(&mut self, offset: i8) {
127        self.i_qp_offset = offset;
128    }
129
130    /// Set P-frame QP offset.
131    pub fn set_p_qp_offset(&mut self, offset: i8) {
132        self.p_qp_offset = offset;
133    }
134
135    /// Set B-frame QP offset.
136    pub fn set_b_qp_offset(&mut self, offset: i8) {
137        self.b_qp_offset = offset;
138    }
139
140    /// Enable or disable adaptive quantization.
141    pub fn set_aq_enabled(&mut self, enabled: bool) {
142        self.enable_aq = enabled;
143    }
144
145    /// Set AQ strength.
146    pub fn set_aq_strength(&mut self, strength: f32) {
147        self.aq_strength = strength.clamp(0.0, 2.0);
148    }
149
150    /// Enable or disable psychovisual optimization.
151    pub fn set_psy_enabled(&mut self, enabled: bool) {
152        self.enable_psy = enabled;
153    }
154
155    /// Set psychovisual strength.
156    pub fn set_psy_strength(&mut self, strength: f32) {
157        self.psy_strength = strength.clamp(0.0, 2.0);
158    }
159
160    /// Set adaptation speed.
161    pub fn set_adaptation_speed(&mut self, speed: f32) {
162        self.adaptation_speed = speed.clamp(0.0, 1.0);
163    }
164
165    /// Select QP for a frame.
166    #[must_use]
167    pub fn select_frame_qp(
168        &mut self,
169        frame_type: FrameType,
170        complexity: f32,
171        target_bits: u64,
172        frame_in_gop: u32,
173    ) -> QpResult {
174        let base_qp = match self.strategy {
175            QpStrategy::Constant => self.select_constant_qp(frame_type),
176            QpStrategy::FrameTypeBased => self.select_frame_type_qp(frame_type),
177            QpStrategy::ComplexityAdaptive => {
178                self.select_complexity_adaptive_qp(frame_type, complexity)
179            }
180            QpStrategy::Psychovisual => {
181                self.select_psychovisual_qp(frame_type, complexity, target_bits)
182            }
183            QpStrategy::RateDistortionOptimized => {
184                self.select_rd_optimized_qp(frame_type, complexity, target_bits)
185            }
186            QpStrategy::Hierarchical => self.select_hierarchical_qp(frame_type, frame_in_gop),
187        };
188
189        // Calculate lambda for RDO
190        let lambda = Self::qp_to_lambda(base_qp);
191        let lambda_me = lambda.sqrt();
192
193        // Generate block-level QP offsets if AQ is enabled
194        let block_qp_offsets = if self.enable_aq {
195            Some(self.generate_aq_offsets(base_qp, complexity))
196        } else {
197            None
198        };
199
200        // Update history
201        self.historical_qp.push(base_qp);
202        if self.historical_qp.len() > self.max_history {
203            self.historical_qp.remove(0);
204        }
205
206        self.frame_count += 1;
207
208        QpResult {
209            qp: base_qp.round() as u8,
210            qp_f: base_qp,
211            lambda,
212            lambda_me,
213            block_qp_offsets,
214            frame_type,
215            complexity_factor: complexity / self.average_complexity(),
216        }
217    }
218
219    /// Select constant QP (with frame type offset).
220    fn select_constant_qp(&self, frame_type: FrameType) -> f32 {
221        let offset = self.get_frame_type_offset(frame_type);
222        (self.base_qp + offset as f32).clamp(self.min_qp as f32, self.max_qp as f32)
223    }
224
225    /// Select frame type based QP.
226    fn select_frame_type_qp(&self, frame_type: FrameType) -> f32 {
227        let offset = self.get_frame_type_offset(frame_type);
228        (self.base_qp + offset as f32).clamp(self.min_qp as f32, self.max_qp as f32)
229    }
230
231    /// Select complexity-adaptive QP.
232    fn select_complexity_adaptive_qp(&self, frame_type: FrameType, complexity: f32) -> f32 {
233        let base = self.select_frame_type_qp(frame_type);
234        let avg_complexity = self.average_complexity();
235
236        if avg_complexity <= 0.0 {
237            return base;
238        }
239
240        // Higher complexity → higher QP to save bits
241        let complexity_ratio = complexity / avg_complexity;
242        let adjustment = (complexity_ratio - 1.0) * 3.0 * self.adaptation_speed;
243
244        (base + adjustment).clamp(self.min_qp as f32, self.max_qp as f32)
245    }
246
247    /// Select psychovisually-optimized QP.
248    fn select_psychovisual_qp(
249        &self,
250        frame_type: FrameType,
251        complexity: f32,
252        _target_bits: u64,
253    ) -> f32 {
254        let base = self.select_complexity_adaptive_qp(frame_type, complexity);
255
256        if !self.enable_psy {
257            return base;
258        }
259
260        // Psychovisual optimization: reduce QP for visually important frames
261        // This is a simplified model - full implementation would analyze spatial features
262        let psy_adjustment = if complexity > 3.0 {
263            // High complexity frames are less visually sensitive
264            self.psy_strength * 0.5
265        } else if complexity < 0.5 {
266            // Low complexity frames are more visually sensitive
267            -self.psy_strength * 0.5
268        } else {
269            0.0
270        };
271
272        (base + psy_adjustment).clamp(self.min_qp as f32, self.max_qp as f32)
273    }
274
275    /// Select rate-distortion optimized QP.
276    fn select_rd_optimized_qp(
277        &self,
278        frame_type: FrameType,
279        complexity: f32,
280        target_bits: u64,
281    ) -> f32 {
282        let base = self.select_complexity_adaptive_qp(frame_type, complexity);
283
284        // Estimate bits at current QP
285        let estimated_bits = self.estimate_bits_at_qp(base, complexity, frame_type);
286
287        if estimated_bits == 0 {
288            return base;
289        }
290
291        // Adjust QP to hit target bits
292        let bits_ratio = estimated_bits as f32 / target_bits as f32;
293
294        // QP adjustment based on rate model
295        // Higher bits_ratio → increase QP to reduce bits
296        let qp_adjustment = if bits_ratio > 1.0 {
297            (bits_ratio.ln() * 6.0).min(5.0)
298        } else {
299            (bits_ratio.ln() * 6.0).max(-5.0)
300        };
301
302        (base + qp_adjustment).clamp(self.min_qp as f32, self.max_qp as f32)
303    }
304
305    /// Select hierarchical QP for B-pyramid structures.
306    fn select_hierarchical_qp(&self, frame_type: FrameType, frame_in_gop: u32) -> f32 {
307        let base = self.select_frame_type_qp(frame_type);
308
309        if frame_type != FrameType::BiDir {
310            return base;
311        }
312
313        // B-frames in higher pyramid levels get higher QP
314        let pyramid_level = self.calculate_pyramid_level(frame_in_gop);
315        let level_offset = pyramid_level as f32 * 1.0;
316
317        (base + level_offset).clamp(self.min_qp as f32, self.max_qp as f32)
318    }
319
320    /// Calculate pyramid level for a frame position.
321    fn calculate_pyramid_level(&self, frame_in_gop: u32) -> u32 {
322        let mut level = 0;
323        let mut pos = frame_in_gop;
324
325        while pos % 2 == 0 && pos > 0 {
326            level += 1;
327            pos /= 2;
328        }
329
330        level
331    }
332
333    /// Get frame type QP offset.
334    fn get_frame_type_offset(&self, frame_type: FrameType) -> i8 {
335        match frame_type {
336            FrameType::Key => self.i_qp_offset,
337            FrameType::Inter => self.p_qp_offset,
338            FrameType::BiDir => self.b_qp_offset,
339            FrameType::Switch => (self.i_qp_offset + self.p_qp_offset) / 2,
340        }
341    }
342
343    /// Generate adaptive quantization offsets for blocks.
344    fn generate_aq_offsets(&self, base_qp: f32, _complexity: f32) -> Vec<f32> {
345        // Simplified AQ: would normally analyze frame content
346        // For now, return empty - full implementation would compute per-block offsets
347        Vec::new()
348    }
349
350    /// Estimate bits needed at a given QP.
351    fn estimate_bits_at_qp(&self, qp: f32, complexity: f32, frame_type: FrameType) -> u64 {
352        // Simplified rate model: bits ≈ complexity * 2^((QP_ref - QP) / 6)
353        // This is based on the relationship between QP and quantization step size
354
355        let base_complexity_bits = complexity * 50_000.0;
356        let qp_ref = 28.0;
357        let qp_factor = 2.0_f32.powf((qp_ref - qp) / 6.0);
358
359        let frame_type_multiplier = match frame_type {
360            FrameType::Key => 3.0,
361            FrameType::Inter => 1.0,
362            FrameType::BiDir => 0.5,
363            FrameType::Switch => 1.5,
364        };
365
366        (base_complexity_bits * qp_factor * frame_type_multiplier) as u64
367    }
368
369    /// Convert QP to lambda for rate-distortion optimization.
370    fn qp_to_lambda(qp: f32) -> f64 {
371        // Standard lambda formula: λ = 0.85 * 2^((QP - 12) / 3)
372        0.85 * 2.0_f64.powf((f64::from(qp) - 12.0) / 3.0)
373    }
374
375    /// Get average QP from history.
376    fn average_qp(&self) -> f32 {
377        if self.historical_qp.is_empty() {
378            return self.base_qp;
379        }
380
381        let sum: f32 = self.historical_qp.iter().sum();
382        sum / self.historical_qp.len() as f32
383    }
384
385    /// Get average complexity estimate.
386    fn average_complexity(&self) -> f32 {
387        // Simplified: assume average complexity of 1.0
388        // Full implementation would track complexity history
389        1.0
390    }
391
392    /// Update selector with actual encoding results.
393    pub fn update(&mut self, actual_qp: f32, actual_bits: u64, target_bits: u64) {
394        // Adapt base QP based on results
395        if target_bits == 0 {
396            return;
397        }
398
399        let bits_ratio = actual_bits as f32 / target_bits as f32;
400
401        // If consistently over/under target, adjust base QP
402        let adjustment = if bits_ratio > 1.1 {
403            0.5 * self.adaptation_speed
404        } else if bits_ratio < 0.8 {
405            -0.5 * self.adaptation_speed
406        } else {
407            0.0
408        };
409
410        self.base_qp = (self.base_qp + adjustment).clamp(self.min_qp as f32, self.max_qp as f32);
411    }
412
413    /// Reset the selector state.
414    pub fn reset(&mut self) {
415        self.historical_qp.clear();
416        self.frame_count = 0;
417    }
418}
419
420/// QP selection result.
421#[derive(Clone, Debug)]
422pub struct QpResult {
423    /// Selected QP (integer).
424    pub qp: u8,
425    /// Selected QP (floating point).
426    pub qp_f: f32,
427    /// Lambda for RDO.
428    pub lambda: f64,
429    /// Lambda for motion estimation.
430    pub lambda_me: f64,
431    /// Block-level QP offsets (if AQ enabled).
432    pub block_qp_offsets: Option<Vec<f32>>,
433    /// Frame type.
434    pub frame_type: FrameType,
435    /// Complexity factor relative to average.
436    pub complexity_factor: f32,
437}
438
439impl QpResult {
440    /// Get effective QP for a block.
441    #[must_use]
442    pub fn get_block_qp(&self, block_index: usize) -> u8 {
443        if let Some(ref offsets) = self.block_qp_offsets {
444            if let Some(&offset) = offsets.get(block_index) {
445                return ((self.qp_f + offset).round() as i32).clamp(1, 63) as u8;
446            }
447        }
448        self.qp
449    }
450
451    /// Check if QP is within acceptable range.
452    #[must_use]
453    pub fn is_valid(&self, min_qp: u8, max_qp: u8) -> bool {
454        self.qp >= min_qp && self.qp <= max_qp
455    }
456}
457
458/// Block-level QP map for adaptive quantization.
459#[derive(Clone, Debug)]
460pub struct BlockQpMap {
461    /// Width in blocks.
462    width: usize,
463    /// Height in blocks.
464    height: usize,
465    /// QP values for each block.
466    qp_values: Vec<u8>,
467    /// Base QP for the frame.
468    base_qp: u8,
469}
470
471impl BlockQpMap {
472    /// Create a new block QP map.
473    #[must_use]
474    pub fn new(width: usize, height: usize, base_qp: u8) -> Self {
475        let qp_values = vec![base_qp; width * height];
476        Self {
477            width,
478            height,
479            qp_values,
480            base_qp,
481        }
482    }
483
484    /// Set QP for a block.
485    pub fn set_block_qp(&mut self, x: usize, y: usize, qp: u8) {
486        if x < self.width && y < self.height {
487            self.qp_values[y * self.width + x] = qp;
488        }
489    }
490
491    /// Get QP for a block.
492    #[must_use]
493    pub fn get_block_qp(&self, x: usize, y: usize) -> u8 {
494        if x < self.width && y < self.height {
495            self.qp_values[y * self.width + x]
496        } else {
497            self.base_qp
498        }
499    }
500
501    /// Apply QP offsets to all blocks.
502    pub fn apply_offsets(&mut self, offsets: &[f32]) {
503        for (i, offset) in offsets.iter().enumerate() {
504            if i < self.qp_values.len() {
505                let new_qp = ((self.base_qp as f32 + offset).round() as i32).clamp(1, 63) as u8;
506                self.qp_values[i] = new_qp;
507            }
508        }
509    }
510
511    /// Get average QP across all blocks.
512    #[must_use]
513    pub fn average_qp(&self) -> f32 {
514        if self.qp_values.is_empty() {
515            return self.base_qp as f32;
516        }
517
518        let sum: u32 = self.qp_values.iter().map(|&qp| u32::from(qp)).sum();
519        sum as f32 / self.qp_values.len() as f32
520    }
521
522    /// Get QP variance.
523    #[must_use]
524    pub fn qp_variance(&self) -> f32 {
525        if self.qp_values.is_empty() {
526            return 0.0;
527        }
528
529        let avg = self.average_qp();
530        let variance: f32 = self
531            .qp_values
532            .iter()
533            .map(|&qp| {
534                let diff = qp as f32 - avg;
535                diff * diff
536            })
537            .sum::<f32>()
538            / self.qp_values.len() as f32;
539
540        variance
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[test]
549    fn test_qp_selector_creation() {
550        let selector = QpSelector::new(28.0, 1, 63);
551        assert_eq!(selector.base_qp, 28.0);
552        assert_eq!(selector.min_qp, 1);
553        assert_eq!(selector.max_qp, 63);
554    }
555
556    #[test]
557    fn test_constant_qp() {
558        let mut selector = QpSelector::new(28.0, 1, 63);
559        selector.set_strategy(QpStrategy::Constant);
560
561        let result = selector.select_frame_qp(FrameType::Inter, 1.0, 100_000, 0);
562        assert_eq!(result.qp, 28);
563
564        let i_result = selector.select_frame_qp(FrameType::Key, 1.0, 300_000, 0);
565        assert_eq!(i_result.qp, 26); // Base 28 + I offset -2
566    }
567
568    #[test]
569    fn test_frame_type_based_qp() {
570        let mut selector = QpSelector::new(28.0, 1, 63);
571        selector.set_strategy(QpStrategy::FrameTypeBased);
572
573        let i_result = selector.select_frame_qp(FrameType::Key, 1.0, 300_000, 0);
574        let p_result = selector.select_frame_qp(FrameType::Inter, 1.0, 100_000, 1);
575        let b_result = selector.select_frame_qp(FrameType::BiDir, 1.0, 50_000, 2);
576
577        assert!(i_result.qp < p_result.qp); // I-frames get lower QP
578        assert!(p_result.qp < b_result.qp); // P-frames get lower QP than B
579    }
580
581    #[test]
582    fn test_complexity_adaptive_qp() {
583        let mut selector = QpSelector::new(28.0, 1, 63);
584        selector.set_strategy(QpStrategy::ComplexityAdaptive);
585
586        let low_complexity = selector.select_frame_qp(FrameType::Inter, 0.5, 100_000, 0);
587        let high_complexity = selector.select_frame_qp(FrameType::Inter, 2.0, 100_000, 1);
588
589        // Higher complexity should get higher QP
590        assert!(high_complexity.qp > low_complexity.qp);
591    }
592
593    #[test]
594    fn test_hierarchical_qp() {
595        let mut selector = QpSelector::new(28.0, 1, 63);
596        selector.set_strategy(QpStrategy::Hierarchical);
597
598        let level0_b = selector.select_frame_qp(FrameType::BiDir, 1.0, 50_000, 2);
599        let level1_b = selector.select_frame_qp(FrameType::BiDir, 1.0, 50_000, 4);
600
601        // Higher pyramid level should get higher QP
602        assert!(level1_b.qp >= level0_b.qp);
603    }
604
605    #[test]
606    fn test_qp_clamping() {
607        let mut selector = QpSelector::new(28.0, 10, 40);
608
609        // Set very low base QP
610        selector.set_base_qp(5.0);
611        assert_eq!(selector.base_qp, 10.0); // Should clamp to min
612
613        // Set very high base QP
614        selector.set_base_qp(50.0);
615        assert_eq!(selector.base_qp, 40.0); // Should clamp to max
616    }
617
618    #[test]
619    fn test_lambda_calculation() {
620        let lambda1 = QpSelector::qp_to_lambda(28.0);
621        let lambda2 = QpSelector::qp_to_lambda(34.0);
622
623        assert!(lambda1 > 0.0);
624        assert!(lambda2 > lambda1); // Higher QP → higher lambda
625    }
626
627    #[test]
628    fn test_qp_adaptation() {
629        let mut selector = QpSelector::new(28.0, 1, 63);
630
631        // Consistently overshooting target
632        for _ in 0..10 {
633            selector.update(28.0, 120_000, 100_000);
634        }
635
636        // Base QP should increase
637        assert!(selector.base_qp > 28.0);
638    }
639
640    #[test]
641    fn test_block_qp_map() {
642        let mut map = BlockQpMap::new(10, 10, 28);
643
644        assert_eq!(map.get_block_qp(5, 5), 28);
645
646        map.set_block_qp(5, 5, 30);
647        assert_eq!(map.get_block_qp(5, 5), 30);
648
649        let avg = map.average_qp();
650        assert!(avg > 28.0); // One block increased to 30
651    }
652
653    #[test]
654    fn test_block_qp_offsets() {
655        let mut map = BlockQpMap::new(4, 4, 28);
656        let offsets = vec![
657            0.0, 1.0, -1.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
658        ];
659
660        map.apply_offsets(&offsets);
661
662        assert_eq!(map.get_block_qp(0, 0), 28);
663        assert_eq!(map.get_block_qp(1, 0), 29);
664        assert_eq!(map.get_block_qp(2, 0), 27);
665        assert_eq!(map.get_block_qp(3, 0), 30);
666    }
667
668    #[test]
669    fn test_qp_variance() {
670        let mut map = BlockQpMap::new(3, 3, 28);
671
672        // All same QP - zero variance
673        let variance1 = map.qp_variance();
674        assert!(variance1 < 0.01);
675
676        // Different QPs - non-zero variance
677        map.set_block_qp(0, 0, 20);
678        map.set_block_qp(1, 1, 36);
679        let variance2 = map.qp_variance();
680        assert!(variance2 > 1.0);
681    }
682
683    #[test]
684    fn test_qp_result() {
685        let result = QpResult {
686            qp: 28,
687            qp_f: 28.0,
688            lambda: 10.0,
689            lambda_me: 3.16,
690            block_qp_offsets: Some(vec![0.0, 1.0, -1.0]),
691            frame_type: FrameType::Inter,
692            complexity_factor: 1.2,
693        };
694
695        assert_eq!(result.get_block_qp(0), 28);
696        assert_eq!(result.get_block_qp(1), 29);
697        assert_eq!(result.get_block_qp(2), 27);
698        assert!(result.is_valid(1, 63));
699    }
700
701    #[test]
702    fn test_reset() {
703        let mut selector = QpSelector::new(28.0, 1, 63);
704
705        let _ = selector.select_frame_qp(FrameType::Inter, 1.0, 100_000, 0);
706        let _ = selector.select_frame_qp(FrameType::Inter, 1.0, 100_000, 1);
707
708        assert!(!selector.historical_qp.is_empty());
709        assert!(selector.frame_count > 0);
710
711        selector.reset();
712
713        assert!(selector.historical_qp.is_empty());
714        assert_eq!(selector.frame_count, 0);
715    }
716}