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
17const MAX_PIXEL_CLOCK_CYCLES: u16 = 15; type Result<T> = core::result::Result<T, &'static str>;
21
22macro_rules! blyat {
24 ($expression:expr, $msg:literal) => {
25 if !$expression {
26 return Result::Err($msg);
27 }
28 };
29}
30
31macro_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 SingleBuffered,
49
50 DoubleBuffered,
56
57 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 pub const fn check(&self) -> bool {
77 self.signal_descr.check()
78 }
79
80 pub const fn display_columns(&self) -> u32 {
82 self.signal_descr.display_columns()
83 }
84
85 pub const fn cpu_ticks_per_field(&self) -> u32 {
90 self.signal_descr.ticks_per_field() * self.pio_prescaler as u32
91 }
92
93 pub const fn display_lines(&self) -> u32 {
97 self.signal_descr.display_lines()
98 }
99
100 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); assert_eq!(self.signal_descr.no_fields(), 2); 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#[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 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#[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 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 number_of_lines_per_field: u32,
413
414 offset: i32,
415
416 fields_per_second: f64,
418 },
419
420 MarginsAndFps {
421 margin_top: f64,
422 margin_bottom: f64,
423
424 fields_per_second: f64,
426 },
427 ExactLineCount(VerticalTiming),
428}
429
430#[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}