rp_cvideo_core/
config.rs

1#![allow(clippy::neg_cmp_op_on_partial_ord)]
2
3#[cfg(test)]
4#[path = "../test/test_config_from_constraints.rs"]
5mod test_constraints;
6
7use crate::buffer::Buffer;
8use crate::buffer_preparation::{LabelAddresses, prepare_buffer};
9use crate::display_buffer::{DisplayBuffer, NoFields, OneField, TwoFields};
10use crate::signal_description::{
11    INTERLACED_PATTERN, PROGRESSIVE_PATTERN, SignalDescription, SignalSection, sync_lines_count,
12};
13use const_soft_float::soft_f64::SoftF64 as SF;
14use core::marker::PhantomData;
15use defmt::Format;
16
17// Based on the size of the delay field in PIO instructions
18const MAX_PIXEL_CLOCK_CYCLES: u16 = 15; // In number of PIO clock cycles
19
20type Result<T> = core::result::Result<T, &'static str>;
21
22// Like assert, but makes you return an Err instead of panicking
23macro_rules! blyat {
24    ($expression:expr, $msg:literal) => {
25        if !$expression {
26            return Result::Err($msg);
27        }
28    };
29}
30
31// Macro that mimics the ? operator's functionality, but const compatible
32macro_rules! blin {
33    ($option:expr) => {
34        match $option {
35            Ok(x) => x,
36            Err(err) => {
37                return Err(err);
38            }
39        }
40    };
41}
42
43#[allow(missing_docs)]
44#[derive(Copy, Clone, Debug, Format)]
45pub enum BufferingMode {
46    /// There will be only one DisplayBuffer which can be drawn on while the vertical-sync portion
47    /// is being streamed
48    SingleBuffered,
49
50    /// There will be two full frames. You can draw on one, while the other one is being streamed.
51    /// After commiting a frame and waiting for the previous one to be finished, you get it back.
52    ///
53    /// Commited buffers will not be modified, so in case of double buffering, the buffer you get back
54    /// does not have the contents of the one you commited last.
55    DoubleBuffered,
56
57    /// A special blend of double-buffering which can only be combined with interlaced video.
58    ///
59    /// With this option, you get to always have one buffer to draw one, but it contains only one field
60    /// of the interlaced buffer, instead of a whole frame consisting of two fields.
61    /// This means you get both the resolution and high refresh-rate of interlaced video.
62    InterlacedSemiDoubleBuffered,
63}
64
65#[allow(missing_docs)]
66#[derive(Copy, Clone, Debug, Format)]
67pub struct CVideoConfig<'t, NF: NoFields> {
68    pub(crate) signal_descr: SignalDescription<'t>,
69    pub(crate) buffering_mode: BufferingMode,
70    pub(crate) pio_prescaler: u16,
71    _nf: PhantomData<NF>,
72}
73
74impl<'t, NF: NoFields> CVideoConfig<'t, NF> {
75    /// Some basic checks on the provided values.
76    pub const fn check(&self) -> bool {
77        self.signal_descr.check()
78    }
79
80    /// The amount of display columns, i.e. the width in number of pixels
81    pub const fn display_columns(&self) -> u32 {
82        self.signal_descr.display_columns()
83    }
84
85    /// The number of CPU clock cycles per field
86    ///
87    /// In case of progressive video, this is the duration for one frame. In case of interlaced, this
88    /// indicates the exact mean duration of the two fields.
89    pub const fn cpu_ticks_per_field(&self) -> u32 {
90        self.signal_descr.ticks_per_field() * self.pio_prescaler as u32
91    }
92
93    /// Pixel height
94    ///
95    /// In case of interlaced, includes both the odd and even lines
96    pub const fn display_lines(&self) -> u32 {
97        self.signal_descr.display_lines()
98    }
99
100    /// Number of u32 words needed for this configuration
101    ///
102    /// Includes optional double buffering. Function is const so this can be used to allocate
103    /// static buffers
104    pub const fn needed_buffer_size(&self) -> usize {
105        let single_buffer_size = self.signal_descr.needed_buffer_size();
106        let multiplication = match self.buffering_mode {
107            BufferingMode::SingleBuffered | BufferingMode::InterlacedSemiDoubleBuffered => 1,
108            BufferingMode::DoubleBuffered => 2,
109        };
110
111        single_buffer_size * multiplication
112    }
113
114    #[doc(hidden)]
115    pub const fn pio_prescaler(&self) -> u16 {
116        self.pio_prescaler
117    }
118
119    #[doc(hidden)]
120    pub fn prepare_buffer<'buf>(
121        &self,
122        buffer: &'buf mut [u32],
123        labels: LabelAddresses,
124    ) -> Result<(Buffer<'buf, NF>, Buffer<'buf, NF>)> {
125        blyat!(
126            buffer.len() >= self.needed_buffer_size(),
127            "Given buffer not big enough"
128        );
129
130        match self.buffering_mode {
131            BufferingMode::SingleBuffered => {
132                let (buffer, _) = prepare_buffer::<NF>(&self.signal_descr, buffer, labels);
133                let split_point = buffer.buffer_pixel_offsets[0];
134
135                let (buffer_rest, buffer_display) = buffer.buffer.split_at_mut(split_point);
136
137                Ok((
138                    Buffer::SyncStuff(buffer_rest),
139                    Buffer::DisplayBuffer(DisplayBuffer {
140                        buffer: buffer_display,
141                        buffer_pixel_offsets: [
142                            0,
143                            buffer.buffer_pixel_offsets[1].saturating_sub(split_point),
144                        ],
145                        columns: buffer.columns,
146                        stride: buffer.stride,
147                        lines: buffer.lines,
148                        _fc: Default::default(),
149                    }),
150                ))
151            }
152
153            BufferingMode::DoubleBuffered => {
154                let (buffer_a, buffer_b) =
155                    buffer.split_at_mut(self.signal_descr.needed_buffer_size());
156                assert!(buffer_b.len() >= self.signal_descr.needed_buffer_size());
157
158                let (buffer_a, _) = prepare_buffer(&self.signal_descr, buffer_a, labels);
159                let (buffer_b, _) = prepare_buffer(&self.signal_descr, buffer_b, labels);
160
161                Ok((
162                    Buffer::DisplayBuffer(buffer_a),
163                    Buffer::DisplayBuffer(buffer_b),
164                ))
165            }
166
167            BufferingMode::InterlacedSemiDoubleBuffered => {
168                assert_eq!(NF::N, OneField::N); // The resulting display buffers have one field each
169                assert_eq!(self.signal_descr.no_fields(), 2); // Make sure this is an interlaced video signal description
170
171                let (buffer, split_at) = prepare_buffer::<NF>(&self.signal_descr, buffer, labels);
172                let (buffer_a, buffer_b) = buffer.buffer.split_at_mut(split_at);
173
174                assert_eq!(buffer.lines % 2, 0);
175
176                Ok((
177                    Buffer::DisplayBuffer(DisplayBuffer {
178                        buffer: buffer_a,
179                        buffer_pixel_offsets: [buffer.buffer_pixel_offsets[0], 0],
180                        columns: buffer.columns,
181                        stride: buffer.stride,
182                        lines: buffer.lines / 2,
183                        _fc: Default::default(),
184                    }),
185                    Buffer::DisplayBuffer(DisplayBuffer {
186                        buffer: buffer_b,
187                        buffer_pixel_offsets: [buffer.buffer_pixel_offsets[1] - split_at, 0],
188                        columns: buffer.columns,
189                        stride: buffer.stride,
190                        lines: buffer.lines / 2,
191                        _fc: Default::default(),
192                    }),
193                ))
194            }
195        }
196    }
197
198    #[doc(hidden)]
199    pub const fn ticks_per_pixel(&self) -> u32 {
200        self.signal_descr.ticks_per_pixel
201    }
202}
203
204/// Standard timings that we probably shouldn't mess with
205///
206/// But we still can :)
207///
208/// All durations are in seconds
209///
210/// There are two standard timings defined as consts: [PAL_STD_DURATIONS] and [NTSC_STD_DURATIONS]
211#[derive(Copy, Clone)]
212pub struct StandardDurations {
213    pub scanline_duration: f64,
214    pub short_vsync_duration: f64,
215    pub long_vsync_duration: f64,
216    pub hsync_pulse_duration: f64,
217    pub min_back_porch_duration: f64,
218    pub min_front_porch_duration: f64,
219}
220
221impl StandardDurations {
222    const fn max_display_length(&self) -> f64 {
223        SF(self.scanline_duration)
224            .sub(SF(self.hsync_pulse_duration))
225            .sub(SF(self.min_back_porch_duration))
226            .sub(SF(self.min_front_porch_duration))
227            .to_f64()
228    }
229
230    /// Does some basic checks on the numbers
231    pub const fn ok(&self) -> bool {
232        self.scanline_duration > 0.0
233            && self.short_vsync_duration > 0.0
234            && self.long_vsync_duration > 0.0
235            && self.hsync_pulse_duration > 0.0
236            && self.min_back_porch_duration > 0.0
237            && self.min_front_porch_duration > 0.0
238            && self.short_vsync_duration < SF(self.scanline_duration).div(SF(2.0)).to_f64()
239            && self.long_vsync_duration < SF(self.scanline_duration).div(SF(2.0)).to_f64()
240            && self.hsync_pulse_duration
241                + self.min_back_porch_duration
242                + self.min_front_porch_duration
243                < self.scanline_duration
244    }
245}
246
247pub const PAL_STD_DURATIONS: StandardDurations = StandardDurations {
248    scanline_duration: 64e-6,
249    short_vsync_duration: 2.35e-6,
250    long_vsync_duration: 27.3e-6,
251    hsync_pulse_duration: 4.7e-6,
252    min_back_porch_duration: 5.7e-6,
253    min_front_porch_duration: 1.65e-6,
254};
255
256pub const NTSC_STD_DURATIONS: StandardDurations = StandardDurations {
257    scanline_duration: 63.55e-6,
258    short_vsync_duration: 2.35e-6,
259    long_vsync_duration: 27.075e-6,
260    hsync_pulse_duration: 4.7e-6,
261    min_back_porch_duration: 4.5e-6,
262    min_front_porch_duration: 1.5e-6,
263};
264
265#[derive(Copy, Clone)]
266pub enum HorizontalConstraint {
267    MarginsAndPixelCount {
268        margin_left: f64,
269        margin_right: f64,
270        pixels: u32,
271    },
272    ExactData(HorizontalTiming),
273}
274
275/// Horizontal timings, in clock ticks
276#[derive(Copy, Clone)]
277pub struct HorizontalTiming {
278    pub scanline: u32,
279    pub vsync_long_pulse: u32,
280    pub vsync_short_pulse: u32,
281    pub hsync_pulse: u32,
282
283    pub pixel_width: u32,
284    pub back_porch: u32,
285    pub front_porch: u32,
286    pub pio_prescaler: u16,
287}
288
289impl HorizontalTiming {
290    const fn from_constraints(
291        constraints: HorizontalConstraint,
292        standard_durations: StandardDurations,
293        cpu_freg: u64,
294    ) -> Result<Self> {
295        const fn ticks(x: SF, f: SF) -> u32 {
296            f.mul(x).round().to_f64() as u32
297        }
298        const fn ticks_per_pixel(display_duration: SF, pio_freq: SF, pixel_count: u32) -> u32 {
299            display_duration
300                .mul(pio_freq)
301                .div(SF(pixel_count as f64))
302                .round()
303                .to_f64() as u32
304        }
305
306        let cpu_freq = SF(cpu_freg as f64);
307
308        match constraints {
309            HorizontalConstraint::MarginsAndPixelCount {
310                margin_left,
311                margin_right,
312                pixels,
313            } => {
314                blyat!(pixels > 0, "Number of pixels is 0");
315                blyat!(
316                    0.0 <= margin_left && margin_left <= 1.0,
317                    "Margin left out of range (0 .. 1)"
318                );
319                blyat!(
320                    0.0 <= margin_right && margin_right <= 1.0,
321                    "Margin right of out range (0 ..1)"
322                );
323
324                let scanline_length = SF(standard_durations.scanline_duration);
325                let max_display_length = SF(standard_durations.max_display_length());
326                let back_porch_length = SF(standard_durations.min_back_porch_duration)
327                    .add(max_display_length.mul(SF(margin_left).div(SF(2.0))));
328                let front_porch_length = SF(standard_durations.min_front_porch_duration)
329                    .add(max_display_length.mul(SF(margin_left).div(SF(2.0))));
330                let hsync_length = SF(standard_durations.hsync_pulse_duration);
331
332                let display_length = scanline_length
333                    .sub(hsync_length)
334                    .sub(back_porch_length)
335                    .sub(front_porch_length);
336
337                blyat!(
338                    ticks_per_pixel(display_length, cpu_freq, pixels) > 0,
339                    "Pixel duration too short for CPU frequency"
340                );
341
342                let mut found_prescaler = None;
343                {
344                    let mut try_prescaler = 1;
345                    'try_loop: while try_prescaler < 1000 {
346                        let ticks = ticks_per_pixel(
347                            display_length,
348                            cpu_freq.div(SF(try_prescaler as f64)),
349                            pixels,
350                        );
351                        if ticks <= MAX_PIXEL_CLOCK_CYCLES as u32 && ticks > 0 {
352                            found_prescaler = Some(try_prescaler);
353                            break 'try_loop;
354                        }
355
356                        try_prescaler += 1
357                    }
358                }
359
360                blyat!(found_prescaler.is_some(), "Could not find PIO prescaler");
361                let pio_prescaler = found_prescaler.unwrap();
362                let pio_freq = cpu_freq.div(SF(pio_prescaler as f64));
363
364                let scanline_ticks = ticks(scanline_length, pio_freq);
365                let back_porch_ticks = ticks(back_porch_length, pio_freq);
366                let front_porch_ticks = ticks(front_porch_length, pio_freq);
367                let hsync_ticks = ticks(hsync_length, pio_freq);
368
369                blyat!(display_length.to_f64() > 0.0, "Scanline too short");
370                let pixel_length = display_length.div(SF(pixels as f64));
371                let corrected_pixel_length = ticks(pixel_length, pio_freq);
372                blyat!(corrected_pixel_length > 0, "Pixel duration is 0");
373                let corrected_display_ticks = corrected_pixel_length * pixels;
374                let scanline_length_missfit = corrected_display_ticks as i32
375                    - (scanline_ticks as i32
376                        - hsync_ticks as i32
377                        - back_porch_ticks as i32
378                        - front_porch_ticks as i32);
379
380                // Adjustments to the back- and front porch needed to keep the whole line
381                // exactly as given with the corrected display length.
382                let offset_back_porch = scanline_length_missfit / 2;
383                let offset_front_porch = scanline_length_missfit - offset_back_porch;
384
385                let corrected_back_porch = back_porch_ticks as i32 - offset_back_porch;
386                let corrected_front_porch = front_porch_ticks as i32 - offset_front_porch;
387
388                blyat!(corrected_back_porch > 0, "Back-porch became 0");
389                blyat!(corrected_front_porch > 0, "Front-porch became 0");
390
391                Ok(Self {
392                    scanline: scanline_ticks,
393                    vsync_long_pulse: ticks(SF(standard_durations.long_vsync_duration), pio_freq),
394                    vsync_short_pulse: ticks(SF(standard_durations.short_vsync_duration), pio_freq),
395                    hsync_pulse: hsync_ticks,
396                    pixel_width: corrected_pixel_length,
397                    back_porch: corrected_back_porch as u32,
398                    front_porch: corrected_front_porch as u32,
399                    pio_prescaler,
400                })
401            }
402
403            HorizontalConstraint::ExactData(x) => Ok(x),
404        }
405    }
406}
407
408#[derive(Copy, Clone)]
409pub enum VerticalConstraint {
410    DisplayLinesAndFps {
411        /// Note: For interlaced video, the number of total lines is double this
412        number_of_lines_per_field: u32,
413
414        offset: i32,
415
416        /// Note: For interlaced video, fields per second is double the frame-rate
417        fields_per_second: f64,
418    },
419
420    MarginsAndFps {
421        margin_top: f64,
422        margin_bottom: f64,
423
424        /// Note: For interlaced video, fields per second is double the frame-rate
425        fields_per_second: f64,
426    },
427    ExactLineCount(VerticalTiming),
428}
429
430/// Vertical timings, in number of lines
431#[derive(Copy, Clone)]
432pub struct VerticalTiming {
433    pub display_lines: u32,
434    pub top_blank_lines: u32,
435    pub bottom_blank_lines: u32,
436}
437
438impl VerticalTiming {
439    const fn from_constraints(
440        constraints: VerticalConstraint,
441        scanline_length_ticks: u32,
442        pio_freq: SF,
443        signal_pattern: &[SignalSection],
444    ) -> Result<Self> {
445        let sync_lines = sync_lines_count(signal_pattern);
446
447        match constraints {
448            VerticalConstraint::DisplayLinesAndFps {
449                number_of_lines_per_field: number_of_display_lines,
450                offset,
451                fields_per_second: fps,
452            } => {
453                blyat!(fps > 0.0, "FPS is 0");
454                let fps = SF(fps);
455                let total_lines = pio_freq
456                    .div(fps.mul(SF(scanline_length_ticks as f64)))
457                    .round()
458                    .to_f64() as u32;
459
460                blyat!(
461                    total_lines > (sync_lines + number_of_display_lines),
462                    "Not enough lines available"
463                );
464                let blank_lines = total_lines - sync_lines - number_of_display_lines;
465
466                blyat!(
467                    (blank_lines / 2) as i32 >= offset,
468                    "Vertical offset too large"
469                );
470                let top_blank_lines = ((blank_lines / 2) as i32 - offset) as u32;
471
472                assert!(blank_lines >= top_blank_lines);
473                let bottom_blank_lines = blank_lines - top_blank_lines;
474
475                Ok(Self {
476                    display_lines: number_of_display_lines,
477                    top_blank_lines,
478                    bottom_blank_lines,
479                })
480            }
481            VerticalConstraint::MarginsAndFps {
482                margin_top,
483                margin_bottom,
484                fields_per_second: fps,
485            } => {
486                blyat!(
487                    0.0 <= margin_top && margin_top <= 1.0,
488                    "Top margin out of range (0 .. 1)"
489                );
490                blyat!(
491                    0.0 <= margin_bottom && margin_bottom <= 1.0,
492                    "Bottom margin out of range (0 .. 1)"
493                );
494
495                blyat!(fps > 0.0, "FPS is 0");
496                let fps = SF(fps);
497                let total_lines = pio_freq
498                    .div(fps.mul(SF(scanline_length_ticks as f64)))
499                    .round()
500                    .to_f64() as u32;
501                let max_display_lines = total_lines - sync_lines;
502                let top_blank_lines = SF(margin_top)
503                    .mul(SF(max_display_lines as f64))
504                    .div(SF(2.0))
505                    .round()
506                    .to_f64() as u32;
507                let bottom_blank_lines = SF(margin_bottom)
508                    .mul(SF(max_display_lines as f64))
509                    .div(SF(2.0))
510                    .round()
511                    .to_f64() as u32;
512
513                blyat!(
514                    total_lines >= top_blank_lines + bottom_blank_lines + sync_lines,
515                    "Not enough lines available"
516                );
517
518                Ok(Self {
519                    display_lines: total_lines - top_blank_lines - bottom_blank_lines - sync_lines,
520                    top_blank_lines,
521                    bottom_blank_lines,
522                })
523            }
524            VerticalConstraint::ExactLineCount(x) => Ok(x),
525        }
526    }
527}
528
529const fn config_from_constraints<NF: NoFields>(
530    buffering_mode: BufferingMode,
531    horizontal: HorizontalConstraint,
532    vertical: VerticalConstraint,
533    standard_durations: StandardDurations,
534    cpu_freq: u64,
535    signal_pattern: &'static [SignalSection],
536) -> Result<CVideoConfig<'static, NF>> {
537    blyat!(cpu_freq > 0, "CPU frequency is 0");
538    blyat!(standard_durations.ok(), "Standard durations not valid");
539
540    let horizontal_timing = blin!(HorizontalTiming::from_constraints(
541        horizontal,
542        standard_durations,
543        cpu_freq
544    ));
545    let pio_freq = SF(cpu_freq as f64).div(SF(horizontal_timing.pio_prescaler as f64));
546
547    let vertical_timing = blin!(VerticalTiming::from_constraints(
548        vertical,
549        horizontal_timing.scanline,
550        pio_freq,
551        signal_pattern
552    ));
553
554    Ok(CVideoConfig {
555        signal_descr: SignalDescription {
556            ticks_per_pixel: horizontal_timing.pixel_width,
557            scanline_length: horizontal_timing.scanline,
558            vsync_long_pulse_length: horizontal_timing.vsync_long_pulse,
559            vsync_short_pulse_length: horizontal_timing.vsync_short_pulse,
560            hsync_pulse_length: horizontal_timing.hsync_pulse,
561            back_porch_length: horizontal_timing.back_porch,
562            front_porch_length: horizontal_timing.front_porch,
563
564            display_lines: vertical_timing.display_lines,
565            top_blank_lines: vertical_timing.top_blank_lines,
566            bottom_blank_lines: vertical_timing.bottom_blank_lines,
567            pattern: signal_pattern,
568        },
569        buffering_mode,
570        pio_prescaler: horizontal_timing.pio_prescaler,
571        _nf: PhantomData::<NF> {},
572    })
573}
574
575impl CVideoConfig<'static, OneField> {
576    pub const fn from_constraints_semi_double_buffered(
577        horizontal: HorizontalConstraint,
578        vertical: VerticalConstraint,
579        standard_durations: StandardDurations,
580        pio_freq: u64,
581    ) -> Result<Self> {
582        config_from_constraints::<OneField>(
583            BufferingMode::InterlacedSemiDoubleBuffered,
584            horizontal,
585            vertical,
586            standard_durations,
587            pio_freq,
588            INTERLACED_PATTERN,
589        )
590    }
591
592    pub const fn from_constraints_progressive(
593        buffering_mode: BufferingMode,
594        horizontal: HorizontalConstraint,
595        vertical: VerticalConstraint,
596        standard_durations: StandardDurations,
597        pio_freq: u64,
598    ) -> Result<Self> {
599        blyat!(
600            !matches!(buffering_mode, BufferingMode::InterlacedSemiDoubleBuffered),
601            "Buffering mode not compatible"
602        );
603        config_from_constraints::<OneField>(
604            buffering_mode,
605            horizontal,
606            vertical,
607            standard_durations,
608            pio_freq,
609            PROGRESSIVE_PATTERN,
610        )
611    }
612}
613
614impl CVideoConfig<'static, TwoFields> {
615    pub const fn from_constraints_interlaced(
616        buffering_mode: BufferingMode,
617        horizontal: HorizontalConstraint,
618        vertical: VerticalConstraint,
619        standard_durations: StandardDurations,
620        pio_freq: u64,
621    ) -> Result<Self> {
622        blyat!(
623            !matches!(buffering_mode, BufferingMode::InterlacedSemiDoubleBuffered),
624            "Buffering mode not compatible"
625        );
626        config_from_constraints::<TwoFields>(
627            buffering_mode,
628            horizontal,
629            vertical,
630            standard_durations,
631            pio_freq,
632            INTERLACED_PATTERN,
633        )
634    }
635}