Skip to main content

device_envoy_core/
memory.rs

1//! In-memory implementations for fast, deterministic native desktop tests.
2//!
3//! Enable the `host` feature when testing in an ordinary Windows, macOS, or
4//! Linux process.
5//!
6//! ## Implementations
7//!
8//! - [`CydMemory`] provides in-memory display and touch through the portable
9//!   [`cyd`](crate::cyd) interfaces.
10//! - [`ButtonMemory`] provides scripted button input.
11//! - [`assert_framebuffer_matches_expected_png`] compares a rendered framebuffer
12//!   with a golden PNG.
13
14#[cfg(test)]
15use core::ops::Range;
16use core::{
17    cell::{Cell, RefCell},
18    convert::Infallible,
19    future::{Future, ready},
20};
21use std::{
22    fs,
23    io::BufWriter,
24    path::{Path, PathBuf},
25    process,
26    rc::Rc,
27    time::{SystemTime, UNIX_EPOCH},
28    vec::Vec,
29};
30
31#[cfg(test)]
32use crate::cyd::backend::TouchUncalibrated;
33#[cfg(test)]
34use crate::cyd::touch::flow::{MIN_SAMPLES_PER_POINT, SAMPLES_DISCARDED_AFTER_DOWN};
35use crate::cyd::{
36    Cyd, CydDisplay, CydTouch,
37    backend::{CalibrationConfig, RawTouchEvent},
38    display::{CydFrame, Orientation},
39    touch::TouchEvent,
40};
41#[cfg(test)]
42use crate::flash_block::{
43    Error as FlashBlockError, FlashBlock, FlashDevice, clear_block, load_block, save_block,
44};
45use crate::{
46    UnwrapInfallible,
47    button::{__ButtonMonitor, Button},
48    pixel_target::{PixelTarget, rgb888_from_rgb565},
49};
50use embedded_graphics::pixelcolor::{Rgb888, RgbColor};
51use embedded_graphics::{
52    Drawable, Pixel,
53    mono_font::{MonoFont, MonoTextStyle, ascii::FONT_9X15_BOLD},
54    pixelcolor::{IntoStorage, Rgb565, raw::RawU16},
55    prelude::{Dimensions, DrawTarget, Point, Size},
56    primitives::Rectangle,
57    text::{Baseline, Text},
58};
59#[cfg(test)]
60use serde::{Deserialize, Serialize};
61
62const DEFAULT_FRAME_BUDGET: usize = 1000;
63#[cfg(test)]
64const FLASH_BLOCK_SIZE: usize = 4096;
65#[cfg(test)]
66const FLASH_BLOCK_OFFSET: u32 = 0;
67#[cfg(test)]
68const FLASH_ERASED_BYTE: u8 = 0xFF;
69
70const fn identity_calibration_config() -> CalibrationConfig {
71    CalibrationConfig::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)
72}
73
74#[derive(Clone)]
75pub(crate) struct FrameClockMemory {
76    frame_index: Rc<Cell<usize>>,
77}
78
79impl FrameClockMemory {
80    #[must_use]
81    pub fn frame_index(&self) -> usize {
82        self.frame_index.get()
83    }
84}
85
86/// Error from the in-memory CYD test surface.
87///
88/// [`OutOfFrames`](Self::OutOfFrames) means that the configured frame budget
89/// has been exhausted. It is returned by a frame flush instead of silently
90/// dropping a rendered frame.
91/// See [`CydMemory::set_frame_budget`].
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum Error {
94    /// The configured number of frame flushes has already been used.
95    /// See [`CydMemory::set_frame_budget`].
96    OutOfFrames,
97}
98#[cfg_attr(
99    feature = "doc-images",
100    doc = ::embed_doc_image::embed_image!("cyd_memory_bitmap", "docs/assets/cyd_memory_bitmap.png")
101)]
102/// In-memory CYD device for fast, deterministic native desktop tests and screenshots.
103///
104/// Enable the `host` feature to use this in an ordinary Windows, macOS, or Linux
105/// process. Tests can draw through the portable [`Cyd`]
106/// interface, inject touch and button input, inspect pixels and flush counts,
107/// and compare the complete framebuffer with a golden PNG.
108///
109/// # Example
110///
111/// ```rust,no_run
112/// use device_envoy_core::{
113///     button::Button,
114///     cyd::{
115///         Cyd, CydDisplay, CydTouch,
116///         display::{CydFrame, DrawItem, Image565Fixed, tga},
117///         touch::TouchEvent,
118///     },
119///     memory::{CydMemory, assert_framebuffer_matches_expected_png},
120/// };
121/// use embedded_graphics::{
122///     mono_font::ascii::FONT_9X15_BOLD,
123///     pixelcolor::{Rgb888, RgbColor},
124///     prelude::{Point, Size},
125/// };
126/// use futures_executor::block_on;
127///
128/// const BITMAP: Image565Fixed<45, 73, { 45 * 73 }> = tga!(concat!(
129///     env!("CARGO_MANIFEST_DIR"),
130///     "/docs/assets/cyd_fill_contiguous.tga"
131/// ))
132/// .to_565();
133///
134/// let mut cyd_memory = CydMemory::new(
135///     Size::new(320, 240),
136///     Rgb888::BLACK,
137///     Rgb888::WHITE,
138///     &FONT_9X15_BOLD,
139/// );
140/// cyd_memory.push_touch_event(TouchEvent::Up);
141/// assert!(matches!(
142///     cyd_memory.touch().try_read()?,
143///     Some(TouchEvent::Up)
144/// ));
145/// let mut button = cyd_memory.button_memory();
146/// button.set_pressed(true);
147/// button.set_pressed_for_frame(1, false);
148/// let mut display = cyd_memory.display();
149/// let mut frame = display.full_frame_mut();
150/// frame.write_text("Hello CYD");
151/// DrawItem::Bitmap {
152///     view: BITMAP.view(),
153///     top_left: Point::new(128, 88),
154/// }
155/// .draw(&mut frame);
156/// block_on(frame.flush())?;
157/// assert!(!button.is_pressed());
158/// assert_eq!(cyd_memory.flush_count(), 1);
159/// let golden_result = assert_framebuffer_matches_expected_png(
160///     &cyd_memory,
161///     env!("CARGO_MANIFEST_DIR"),
162///     "cyd_memory_bitmap.png",
163/// );
164/// assert!(golden_result.is_ok(), "{golden_result:?}");
165/// # Ok::<(), device_envoy_core::memory::Error>(())
166/// ```
167///
168/// ![CydMemory framebuffer preview][cyd_memory_bitmap]
169pub struct CydMemory {
170    display: CydDisplayMemory,
171    touch: CydTouchMemory,
172    shared: Rc<RefCell<CydMemoryShared>>,
173    orientation: Orientation,
174}
175
176struct CydMemoryShared {
177    framebuffer: Vec<u16>,
178    flush_count: usize,
179    last_flush_rectangle: Option<Rectangle>,
180    frame_budget: usize,
181    raw_touch_script: FrameScript<RawTouchEvent>,
182    touch_script: FrameScript<TouchEvent>,
183    frame_clock: FrameClockMemory,
184}
185
186/// Owned display half of [`CydMemory`].
187#[derive(Clone)]
188pub struct CydDisplayMemory {
189    size: Size,
190    background_color: Rgb888,
191    foreground_color: Rgb888,
192    background565: Rgb565,
193    foreground565: Rgb565,
194    font: &'static MonoFont<'static>,
195    shared: Rc<RefCell<CydMemoryShared>>,
196}
197
198/// Owned calibrated touch half of [`CydMemory`].
199#[derive(Clone)]
200pub struct CydTouchMemory {
201    shared: Rc<RefCell<CydMemoryShared>>,
202    calibration_config: CalibrationConfig,
203}
204
205/// Owned uncalibrated touch half used by calibration tests.
206#[cfg(test)]
207pub(crate) struct CydTouchUncalibratedMemory {
208    shared: Rc<RefCell<CydMemoryShared>>,
209}
210
211/// In-progress in-memory frame that flushes into an in-memory framebuffer.
212pub struct CydFrameMemory {
213    shared: Rc<RefCell<CydMemoryShared>>,
214    screen_size: Size,
215    rectangle: Rectangle,
216    background565: Rgb565,
217    foreground565: Rgb565,
218    font: &'static MonoFont<'static>,
219    pixels: Vec<u16>,
220}
221
222struct FrameScript<Event> {
223    current_frame: Vec<Event>,
224    future_frames: Vec<Vec<Event>>,
225    current_read_index: usize,
226}
227
228#[cfg(test)]
229pub(crate) struct FlashBlockMemory {
230    flash_device_memory: FlashDeviceMemory,
231    save_count: usize,
232}
233
234#[cfg(test)]
235struct FlashDeviceMemory {
236    bytes: [u8; FLASH_BLOCK_SIZE],
237}
238
239/// Native desktop button test double returned by [`CydMemory::button_memory`].
240///
241/// # Example
242///
243/// ```rust,no_run
244/// use device_envoy_core::{button::Button, memory::ButtonMemory};
245///
246/// let mut button = ButtonMemory::new();
247/// assert!(!button.is_pressed());
248/// button.set_pressed(true);
249/// assert!(button.is_pressed());
250/// ```
251pub struct ButtonMemory {
252    pressed: bool,
253    pressed_frames: Vec<(usize, bool)>,
254    frame_clock: Option<FrameClockMemory>,
255}
256
257impl CydMemory {
258    /// Construct an empty in-memory CYD surface with the given screen style.
259    ///
260    /// The [`CydMemory` example](CydMemory) demonstrates the canonical
261    /// construction and complete host-test workflow.
262    #[must_use]
263    pub fn new(
264        size: Size,
265        background_color: Rgb888,
266        foreground_color: Rgb888,
267        font: &'static MonoFont<'static>,
268    ) -> Self {
269        let orientation = if size.width > size.height {
270            Orientation::Landscape
271        } else {
272            Orientation::Portrait
273        };
274        Self::new_inner(size, orientation, background_color, foreground_color, font)
275    }
276
277    /// Construct an in-memory CYD surface with an oriented logical screen.
278    ///
279    /// # Example
280    ///
281    /// ```rust,no_run
282    /// use device_envoy_core::{
283    ///     cyd::{Cyd, CydDisplay, display::{CydFrame, Orientation}},
284    ///     memory::{CydMemory, Error},
285    /// };
286    /// use embedded_graphics::{
287    ///     mono_font::ascii::FONT_9X15_BOLD,
288    ///     pixelcolor::{Rgb565, Rgb888},
289    ///     prelude::{Point, RgbColor, Size},
290    ///     primitives::Rectangle,
291    /// };
292    /// use futures_executor::block_on;
293    ///
294    /// let mut cyd_memory = CydMemory::new_with_orientation(
295    ///     Orientation::LandscapeInverted,
296    ///     Rgb888::BLACK,
297    ///     Rgb888::WHITE,
298    ///     &FONT_9X15_BOLD,
299    /// );
300    /// assert_eq!(cyd_memory.orientation(), Orientation::LandscapeInverted);
301    ///
302    /// let pixel = Rectangle::new(Point::zero(), Size::new(1, 1));
303    /// let mut display = cyd_memory.display();
304    /// let mut first_frame = display.frame_mut(pixel);
305    /// first_frame.fill(Rgb565::RED);
306    /// block_on(first_frame.flush())?;
307    /// drop(first_frame);
308    /// assert_eq!(cyd_memory.pixel(0, 0), Rgb565::RED);
309    /// cyd_memory.rotate_framebuffer_180();
310    /// assert_eq!(cyd_memory.pixel(319, 239), Rgb565::RED);
311    /// # Ok::<(), Error>(())
312    /// ```
313    #[must_use]
314    pub fn new_with_orientation(
315        orientation: Orientation,
316        background_color: Rgb888,
317        foreground_color: Rgb888,
318        font: &'static MonoFont<'static>,
319    ) -> Self {
320        Self::new_inner(
321            orientation.size(),
322            orientation,
323            background_color,
324            foreground_color,
325            font,
326        )
327    }
328
329    fn new_inner(
330        size: Size,
331        orientation: Orientation,
332        background_color: Rgb888,
333        foreground_color: Rgb888,
334        font: &'static MonoFont<'static>,
335    ) -> Self {
336        let background565 = Rgb565::from(background_color);
337        let pixel_count = size.width as usize * size.height as usize;
338        let shared = Rc::new(RefCell::new(CydMemoryShared {
339            framebuffer: vec![background565.into_storage(); pixel_count],
340            flush_count: 0,
341            last_flush_rectangle: None,
342            frame_budget: DEFAULT_FRAME_BUDGET,
343            raw_touch_script: FrameScript::default(),
344            touch_script: FrameScript::default(),
345            frame_clock: FrameClockMemory {
346                frame_index: Rc::new(Cell::new(0)),
347            },
348        }));
349        let display = CydDisplayMemory {
350            size,
351            background_color,
352            foreground_color,
353            background565,
354            foreground565: Rgb565::from(foreground_color),
355            font,
356            shared: shared.clone(),
357        };
358        let touch = CydTouchMemory {
359            shared: shared.clone(),
360            calibration_config: identity_calibration_config(),
361        };
362        Self {
363            display,
364            touch,
365            shared,
366            orientation,
367        }
368    }
369
370    #[must_use]
371    /// Clone the device's display component for an independent test task.
372    pub fn display(&self) -> CydDisplayMemory {
373        self.display.clone()
374    }
375
376    /// Clone owned calibrated parts that share this harness's backing state.
377    #[must_use]
378    pub fn owned_parts(&self) -> (CydDisplayMemory, CydTouchMemory) {
379        (self.display.clone(), self.touch.clone())
380    }
381
382    #[must_use]
383    #[cfg(test)]
384    pub(crate) fn parts_uncalibrated(&self) -> (CydDisplayMemory, CydTouchUncalibratedMemory) {
385        (
386            self.display.clone(),
387            CydTouchUncalibratedMemory {
388                shared: Rc::clone(&self.touch.shared),
389            },
390        )
391    }
392}
393
394impl Cyd for CydMemory {
395    type Error = Error;
396    type Display = CydDisplayMemory;
397    type Touch = CydTouchMemory;
398
399    fn parts(&mut self) -> (&mut Self::Display, &mut Self::Touch) {
400        (&mut self.display, &mut self.touch)
401    }
402
403    fn orientation(&self) -> Orientation {
404        self.orientation
405    }
406}
407
408impl CydMemory {
409    /// Limit how many frames may flush before [`Error::OutOfFrames`].
410    ///
411    /// ```rust,no_run
412    /// use device_envoy_core::{
413    ///     cyd::{CydDisplay, display::CydFrame},
414    ///     memory::{CydMemory, Error},
415    /// };
416    /// use embedded_graphics::{
417    ///     mono_font::ascii::FONT_9X15_BOLD,
418    ///     pixelcolor::{Rgb888, RgbColor},
419    ///     prelude::Size,
420    /// };
421    ///
422    /// let mut cyd_memory = CydMemory::new(
423    ///     Size::new(320, 240),
424    ///     Rgb888::BLACK,
425    ///     Rgb888::WHITE,
426    ///     &FONT_9X15_BOLD,
427    /// );
428    /// cyd_memory.set_frame_budget(1);
429    /// let mut display = cyd_memory.display();
430    ///
431    /// let mut first_frame = display.full_frame_mut();
432    /// futures_executor::block_on(first_frame.flush())?;
433    /// drop(first_frame);
434    /// let mut second_frame = display.full_frame_mut();
435    /// assert_eq!(
436    ///     futures_executor::block_on(second_frame.flush()),
437    ///     Err(Error::OutOfFrames),
438    /// );
439    /// # Ok::<(), Error>(())
440    /// ```
441    pub fn set_frame_budget(&mut self, frame_budget: usize) {
442        self.shared.borrow_mut().frame_budget = frame_budget;
443    }
444
445    #[must_use]
446    pub(crate) fn frame_clock(&self) -> FrameClockMemory {
447        self.shared.borrow().frame_clock.clone()
448    }
449
450    /// Create a native desktop test button tied to this device's frame clock.
451    ///
452    /// The [`CydMemory` example](CydMemory) demonstrates button state changing
453    /// when a frame flush advances the shared clock.
454    #[must_use]
455    pub fn button_memory(&self) -> ButtonMemory {
456        ButtonMemory::with_frame_clock(self.frame_clock())
457    }
458
459    #[cfg(test)]
460    pub(crate) fn script_raw_frames(&mut self, raw_touch_frames: &[&[RawTouchEvent]]) {
461        self.shared
462            .borrow_mut()
463            .raw_touch_script
464            .replace_frames(raw_touch_frames);
465    }
466
467    #[cfg(test)]
468    pub(crate) fn script_raw_frames_owned(&mut self, raw_touch_frames: Vec<Vec<RawTouchEvent>>) {
469        self.shared
470            .borrow_mut()
471            .raw_touch_script
472            .replace_owned_frames(raw_touch_frames);
473    }
474
475    #[cfg(test)]
476    pub(crate) fn push_raw_touch_event(&mut self, raw_touch_event: RawTouchEvent) {
477        self.shared
478            .borrow_mut()
479            .raw_touch_script
480            .push_current_frame_event(raw_touch_event);
481    }
482
483    /// Queue one calibrated touch event for the current frame.
484    ///
485    /// The [`CydMemory` example](CydMemory) demonstrates injecting an event and
486    /// reading it through the portable [`CydTouch`] API.
487    pub fn push_touch_event(&mut self, touch_event: TouchEvent) {
488        self.shared
489            .borrow_mut()
490            .touch_script
491            .push_current_frame_event(touch_event);
492    }
493
494    /// Return how many frames have flushed so far.
495    #[must_use]
496    pub fn flush_count(&self) -> usize {
497        self.shared.borrow().flush_count
498    }
499
500    /// Return the rectangle flushed most recently, if any.
501    #[must_use]
502    pub fn last_flush_rectangle(&self) -> Option<Rectangle> {
503        self.shared.borrow().last_flush_rectangle
504    }
505
506    /// Read one pixel from the in-memory framebuffer.
507    #[must_use]
508    pub fn pixel(&self, position_x: usize, position_y: usize) -> Rgb565 {
509        assert!(
510            position_x < self.display.size.width as usize,
511            "position_x must stay within the screen"
512        );
513        assert!(
514            position_y < self.display.size.height as usize,
515            "position_y must stay within the screen"
516        );
517        let stride = self.display.size.width as usize;
518        let shared = self.shared.borrow();
519        Rgb565::from(RawU16::new(
520            shared.framebuffer[position_y * stride + position_x],
521        ))
522    }
523
524    /// Apply the physical 180-degree presentation used by an inverted CYD orientation.
525    ///
526    /// The [`new_with_orientation`](CydMemory::new_with_orientation) example
527    /// demonstrates rotating an inverted framebuffer and inspecting a pixel.
528    ///
529    /// Hardware display drivers and browser shells apply this transform outside the logical
530    /// application framebuffer. Native desktop previews can call this after rendering to compare the
531    /// user-facing presentation rather than the untransformed logical buffer.
532    #[cfg(feature = "host")]
533    pub fn rotate_framebuffer_180(&self) {
534        let mut shared = self.shared.borrow_mut();
535        let width = self.display.size.width as usize;
536        let height = self.display.size.height as usize;
537        for row_index in 0..height / 2 {
538            let opposite_row_index = height - 1 - row_index;
539            for column_index in 0..width {
540                let first_index = row_index * width + column_index;
541                let second_index = opposite_row_index * width + (width - 1 - column_index);
542                shared.framebuffer.swap(first_index, second_index);
543            }
544        }
545        if height % 2 == 1 {
546            let row_start = (height / 2) * width;
547            let row_end = row_start + width;
548            shared.framebuffer[row_start..row_end].reverse();
549        }
550    }
551
552    /// Write the framebuffer as an RGB PNG for native desktop previews and assertions.
553    pub(crate) fn write_framebuffer_png(
554        &self,
555        path: impl AsRef<Path>,
556    ) -> Result<(), Box<dyn std::error::Error>> {
557        let width = self.display.size.width;
558        let height = self.display.size.height;
559        let mut rgb_bytes = Vec::with_capacity(width as usize * height as usize * 3);
560        let shared = self.shared.borrow();
561        for pixel in &shared.framebuffer {
562            let color = rgb888_from_rgb565(*pixel);
563            rgb_bytes.push(color.r());
564            rgb_bytes.push(color.g());
565            rgb_bytes.push(color.b());
566        }
567
568        let path = path.as_ref();
569        if let Some(parent) = path.parent() {
570            fs::create_dir_all(parent)?;
571        }
572        let file = fs::File::create(path)?;
573        let writer = BufWriter::new(file);
574        let mut encoder = png::Encoder::new(writer, width, height);
575        encoder.set_color(png::ColorType::Rgb);
576        encoder.set_depth(png::BitDepth::Eight);
577        let mut png_writer = encoder.write_header()?;
578        png_writer.write_image_data(&rgb_bytes)?;
579        Ok(())
580    }
581}
582
583/// Compare a rendered [`CydMemory`] framebuffer with an expected PNG.
584///
585/// The [`CydMemory` example](CydMemory) demonstrates the complete golden-image
586/// workflow.
587///
588/// # Expected image
589///
590/// Pass `env!("CARGO_MANIFEST_DIR")` as `manifest_dir` to compare against:
591///
592/// `<manifest_dir>/tests/assets/<relative_filename>`
593///
594/// # Updating the expected image
595///
596/// Set `DEVICE_ENVOY_UPDATE_CYD_PNGS=1` to replace the expected PNG with the
597/// current framebuffer. Use this only when accepting an intentional visual
598/// change.
599///
600/// # Exporting a preview
601///
602/// Set `DEVICE_ENVOY_PREVIEW_OUTPUT_PATH` to also write the current framebuffer
603/// to another path. The normal comparison or update behavior still runs.
604///
605/// # Errors
606///
607/// Returns an error when PNG encoding or file access fails, when the expected
608/// image does not exist, or when its bytes differ from the current framebuffer.
609pub fn assert_framebuffer_matches_expected_png(
610    cyd_memory: &CydMemory,
611    manifest_dir: &str,
612    relative_filename: &str,
613) -> Result<(), Box<dyn std::error::Error>> {
614    // The Pages xtask `build-pages` command reuses these exact tests to render gallery
615    // preview images: no browser needed, since this crate already renders
616    // the real example logic onto a native in-memory framebuffer. Set this
617    // env var to also copy the freshly rendered frame out to an arbitrary
618    // path, on top of (not instead of) the normal golden-image comparison
619    // below, so a preview build still catches a real rendering regression.
620    if let Some(preview_output_path) = std::env::var_os("DEVICE_ENVOY_PREVIEW_OUTPUT_PATH") {
621        cyd_memory.write_framebuffer_png(preview_output_path)?;
622    }
623
624    let mut expected_path = PathBuf::from(manifest_dir);
625    expected_path.push("tests");
626    expected_path.push("assets");
627    expected_path.push(relative_filename);
628
629    if std::env::var_os("DEVICE_ENVOY_UPDATE_CYD_PNGS").is_some() {
630        cyd_memory.write_framebuffer_png(&expected_path)?;
631        std::println!("updated PNG at {}", expected_path.display());
632        return Ok(());
633    }
634
635    if !expected_path.exists() {
636        return Err(std::format!(
637            "expected PNG is missing at {}; rerun with DEVICE_ENVOY_UPDATE_CYD_PNGS=1 to create it",
638            expected_path.display()
639        )
640        .into());
641    }
642
643    let unix_nanos = SystemTime::now()
644        .duration_since(UNIX_EPOCH)
645        .map(|duration| duration.as_nanos())
646        .unwrap_or(0);
647    let temp_path = std::env::temp_dir().join(std::format!(
648        "{}-{}-{unix_nanos}",
649        relative_filename.replace('/', "_"),
650        process::id()
651    ));
652    cyd_memory.write_framebuffer_png(&temp_path)?;
653
654    let expected_bytes = fs::read(&expected_path)?;
655    let actual_bytes = fs::read(&temp_path)?;
656    if let Err(error) = fs::remove_file(&temp_path)
657        && error.kind() != std::io::ErrorKind::NotFound
658    {
659        return Err(error.into());
660    }
661
662    if expected_bytes != actual_bytes {
663        return Err(std::format!(
664            "PNG bytes differ from {}; rerun with DEVICE_ENVOY_UPDATE_CYD_PNGS=1 to accept the new image",
665            expected_path.display()
666        )
667        .into());
668    }
669    Ok(())
670}
671
672impl Default for CydMemory {
673    fn default() -> Self {
674        Self::new(
675            Size::new(320, 240),
676            Rgb888::BLACK,
677            Rgb888::WHITE,
678            &FONT_9X15_BOLD,
679        )
680    }
681}
682
683impl core::fmt::Debug for CydMemory {
684    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
685        formatter.debug_struct("CydMemory").finish_non_exhaustive()
686    }
687}
688
689impl core::fmt::Debug for CydTouchMemory {
690    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
691        formatter
692            .debug_struct("CydTouchMemory")
693            .field("calibration_config", &self.calibration_config)
694            .finish_non_exhaustive()
695    }
696}
697
698#[cfg(test)]
699impl core::fmt::Debug for CydTouchUncalibratedMemory {
700    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
701        formatter
702            .debug_struct("CydTouchUncalibratedMemory")
703            .finish_non_exhaustive()
704    }
705}
706
707#[cfg(test)]
708impl TouchUncalibrated for CydTouchUncalibratedMemory {
709    type Error = Error;
710    type Calibrated = CydTouchMemory;
711
712    fn read_raw_touch_event(&mut self) -> Result<Option<RawTouchEvent>, Self::Error> {
713        Ok(self
714            .shared
715            .borrow_mut()
716            .raw_touch_script
717            .pop_current_frame_event())
718    }
719
720    fn calibrate(
721        self,
722        calibration_config: CalibrationConfig,
723        _orientation: Orientation,
724    ) -> Self::Calibrated {
725        CydTouchMemory {
726            shared: self.shared,
727            calibration_config,
728        }
729    }
730}
731
732impl crate::cyd::backend::DisplayBackend for CydDisplayMemory {
733    type Error = Error;
734
735    type Frame<'a> = CydFrameMemory;
736
737    fn create_frame_mut(&mut self, rectangle: Rectangle) -> Self::Frame<'_> {
738        let pixel_count = rectangle.size.width as usize * rectangle.size.height as usize;
739        CydFrameMemory {
740            shared: self.shared.clone(),
741            screen_size: self.size,
742            rectangle,
743            background565: self.background565,
744            foreground565: self.foreground565,
745            font: self.font,
746            pixels: vec![self.background565.into_storage(); pixel_count],
747        }
748    }
749}
750
751impl CydDisplay for CydDisplayMemory {
752    fn screen_size(&self) -> Size {
753        self.size
754    }
755
756    fn background_color(&self) -> Rgb888 {
757        self.background_color
758    }
759
760    fn foreground_color(&self) -> Rgb888 {
761        self.foreground_color
762    }
763
764    fn background_565(&self) -> Rgb565 {
765        self.background565
766    }
767
768    fn foreground_565(&self) -> Rgb565 {
769        self.foreground565
770    }
771
772    fn fill_rectangle(&mut self, rectangle: Rectangle, color: Rgb565) -> Result<(), Self::Error> {
773        fill_rectangle_in_framebuffer(
774            &mut self.shared.borrow_mut().framebuffer,
775            self.size,
776            rectangle,
777            color.into_storage(),
778        );
779        Ok(())
780    }
781
782    fn fill_contiguous<I>(&mut self, rectangle: Rectangle, pixels: I) -> Result<(), Self::Error>
783    where
784        I: IntoIterator<Item = Rgb565>,
785    {
786        fill_contiguous_in_framebuffer(
787            &mut self.shared.borrow_mut().framebuffer,
788            self.size,
789            rectangle,
790            pixels.into_iter().map(IntoStorage::into_storage),
791        );
792        Ok(())
793    }
794}
795
796impl CydTouch for CydTouchMemory {
797    type Error = Error;
798
799    fn try_read(&mut self) -> Result<Option<TouchEvent>, Self::Error> {
800        Ok(self
801            .shared
802            .borrow_mut()
803            .touch_script
804            .pop_current_frame_event())
805    }
806}
807
808impl CydFrameMemory {
809    fn width(&self) -> usize {
810        self.rectangle.size.width as usize
811    }
812
813    fn height(&self) -> usize {
814        self.rectangle.size.height as usize
815    }
816
817    fn local_x(&self, position_x: i32) -> Option<usize> {
818        usize::try_from(position_x.checked_sub(self.rectangle.top_left.x)?).ok()
819    }
820
821    fn local_y(&self, position_y: i32) -> Option<usize> {
822        usize::try_from(position_y.checked_sub(self.rectangle.top_left.y)?).ok()
823    }
824
825    fn flush_now(&mut self) -> Result<(), Error> {
826        let mut shared = self.shared.borrow_mut();
827        if shared.flush_count >= shared.frame_budget {
828            return Err(Error::OutOfFrames);
829        }
830
831        blit_frame_to_screen(
832            &mut shared.framebuffer,
833            self.screen_size,
834            self.rectangle,
835            &self.pixels,
836        );
837        shared.last_flush_rectangle = Some(self.rectangle);
838        shared.flush_count += 1;
839        shared.raw_touch_script.advance_frame();
840        shared.touch_script.advance_frame();
841        shared
842            .frame_clock
843            .frame_index
844            .set(shared.frame_clock.frame_index.get() + 1);
845        Ok(())
846    }
847}
848
849impl DrawTarget for CydFrameMemory {
850    type Color = Rgb565;
851    type Error = Infallible;
852
853    fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
854        self.fill(color);
855        Ok(())
856    }
857
858    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
859    where
860        I: IntoIterator<Item = Pixel<Self::Color>>,
861    {
862        for Pixel(point, color) in pixels {
863            let Some(local_x) = self.local_x(point.x) else {
864                continue;
865            };
866            let Some(local_y) = self.local_y(point.y) else {
867                continue;
868            };
869            if local_x >= self.width() || local_y >= self.height() {
870                continue;
871            }
872            let stride = self.width();
873            self.pixels[local_y * stride + local_x] = color.into_storage();
874        }
875        Ok(())
876    }
877}
878
879impl Dimensions for CydFrameMemory {
880    fn bounding_box(&self) -> Rectangle {
881        self.rectangle
882    }
883}
884
885impl PixelTarget for CydFrameMemory {
886    fn width(&self) -> usize {
887        usize::try_from(self.rectangle.top_left.x)
888            .expect("frame top-left x must be non-negative")
889            .checked_add(self.width())
890            .expect("frame width must fit in usize")
891    }
892
893    fn height(&self) -> usize {
894        usize::try_from(self.rectangle.top_left.y)
895            .expect("frame top-left y must be non-negative")
896            .checked_add(self.height())
897            .expect("frame height must fit in usize")
898    }
899
900    fn put_pixel(&mut self, x: usize, y: usize, color: Rgb888) {
901        self.put_pixel_565(x, y, Rgb565::from(color).into_storage());
902    }
903
904    fn put_pixel_565(&mut self, x: usize, y: usize, rgb565: u16) {
905        let Some(local_x) = self.local_x(x as i32) else {
906            return;
907        };
908        let Some(local_y) = self.local_y(y as i32) else {
909            return;
910        };
911        if local_x >= self.width() || local_y >= self.height() {
912            return;
913        }
914        let stride = self.width();
915        self.pixels[local_y * stride + local_x] = rgb565;
916    }
917}
918
919impl CydFrame for CydFrameMemory {
920    type Error = Error;
921
922    fn rectangle(&self) -> Rectangle {
923        self.rectangle
924    }
925
926    fn fill(&mut self, color: Rgb565) -> &mut Self {
927        self.pixels.fill(color.into_storage());
928        self
929    }
930
931    fn clear(&mut self) -> &mut Self {
932        self.fill(self.background565)
933    }
934
935    fn write_text(&mut self, text: &str) -> &mut Self {
936        Text::with_baseline(
937            text,
938            self.rectangle.top_left,
939            MonoTextStyle::new(self.font, self.foreground565),
940            Baseline::Top,
941        )
942        .draw(self)
943        .unwrap_infallible();
944        self
945    }
946
947    fn copy_from_565(&mut self, src: &[u16]) -> crate::Result<()> {
948        if self.pixels.len() != src.len() {
949            return Err(crate::Error::CopySize {
950                src_len: src.len(),
951                frame_len: self.pixels.len(),
952            });
953        }
954        self.pixels.copy_from_slice(src);
955        Ok(())
956    }
957
958    fn flush(&mut self) -> impl Future<Output = Result<(), <Self as CydFrame>::Error>> {
959        ready(self.flush_now())
960    }
961}
962
963impl<Event> Default for FrameScript<Event> {
964    fn default() -> Self {
965        Self {
966            current_frame: Vec::new(),
967            future_frames: Vec::new(),
968            current_read_index: 0,
969        }
970    }
971}
972
973impl<Event: Clone> FrameScript<Event> {
974    #[cfg(test)]
975    fn replace_frames(&mut self, frames: &[&[Event]]) {
976        self.current_frame.clear();
977        self.future_frames.clear();
978        self.current_read_index = 0;
979        if let Some((first_frame, remaining_frames)) = frames.split_first() {
980            self.current_frame = first_frame.to_vec();
981            self.future_frames = remaining_frames
982                .iter()
983                .map(|frame| frame.to_vec())
984                .collect();
985        }
986    }
987
988    #[cfg(test)]
989    fn replace_owned_frames(&mut self, mut frames: Vec<Vec<Event>>) {
990        self.current_frame.clear();
991        self.future_frames.clear();
992        self.current_read_index = 0;
993        if frames.is_empty() {
994            return;
995        }
996        self.current_frame = frames.remove(0);
997        self.future_frames = frames;
998    }
999
1000    fn push_current_frame_event(&mut self, event: Event) {
1001        self.current_frame.push(event);
1002    }
1003
1004    fn pop_current_frame_event(&mut self) -> Option<Event> {
1005        let event = self.current_frame.get(self.current_read_index).cloned();
1006        if event.is_some() {
1007            self.current_read_index += 1;
1008        }
1009        event
1010    }
1011
1012    fn advance_frame(&mut self) {
1013        if self.current_read_index >= self.current_frame.len() {
1014            if let Some(next_frame) = self.future_frames.first().cloned() {
1015                self.current_frame = next_frame;
1016                self.future_frames.remove(0);
1017            } else {
1018                self.current_frame.clear();
1019            }
1020            self.current_read_index = 0;
1021            return;
1022        }
1023
1024        self.current_frame.drain(0..self.current_read_index);
1025        self.current_read_index = 0;
1026    }
1027}
1028
1029#[cfg(test)]
1030impl FlashBlockMemory {
1031    #[must_use]
1032    pub fn new() -> Self {
1033        Self {
1034            flash_device_memory: FlashDeviceMemory::new(),
1035            save_count: 0,
1036        }
1037    }
1038
1039    #[must_use]
1040    pub fn with_value<T>(value: &T) -> Self
1041    where
1042        T: Serialize + for<'de> Deserialize<'de>,
1043    {
1044        let mut flash_block_memory = Self::new();
1045        flash_block_memory
1046            .save(value)
1047            .expect("saving a small in-memory flash value should succeed");
1048        flash_block_memory
1049    }
1050
1051    #[must_use]
1052    pub fn with_raw_bytes(bytes: &[u8]) -> Self {
1053        let mut flash_block_memory = Self::new();
1054        flash_block_memory
1055            .flash_device_memory
1056            .write_raw_bytes(bytes);
1057        flash_block_memory
1058    }
1059
1060    #[must_use]
1061    pub fn save_count(&self) -> usize {
1062        self.save_count
1063    }
1064}
1065
1066#[cfg(test)]
1067impl Default for FlashBlockMemory {
1068    fn default() -> Self {
1069        Self::new()
1070    }
1071}
1072
1073#[cfg(test)]
1074impl FlashBlock for FlashBlockMemory {
1075    type Error = FlashBlockError<Infallible>;
1076
1077    fn load<T>(&mut self) -> Result<Option<T>, Self::Error>
1078    where
1079        T: Serialize + for<'de> Deserialize<'de>,
1080    {
1081        match load_block::<FLASH_BLOCK_SIZE, T, _>(
1082            &mut self.flash_device_memory,
1083            FLASH_BLOCK_OFFSET,
1084        ) {
1085            Ok(value) => Ok(value),
1086            Err(FlashBlockError::StorageCorrupted | FlashBlockError::FormatError) => Ok(None),
1087            Err(FlashBlockError::Io(infallible)) => match infallible {},
1088        }
1089    }
1090
1091    fn save<T>(&mut self, value: &T) -> Result<(), Self::Error>
1092    where
1093        T: Serialize + for<'de> Deserialize<'de>,
1094    {
1095        save_block::<FLASH_BLOCK_SIZE, _, _>(
1096            &mut self.flash_device_memory,
1097            FLASH_BLOCK_OFFSET,
1098            value,
1099        )?;
1100        self.save_count += 1;
1101        Ok(())
1102    }
1103
1104    fn clear(&mut self) -> Result<(), Self::Error> {
1105        clear_block::<FLASH_BLOCK_SIZE, _>(&mut self.flash_device_memory, FLASH_BLOCK_OFFSET)
1106    }
1107}
1108
1109#[cfg(test)]
1110impl FlashDeviceMemory {
1111    // TODO Consider consolidating this native desktop flash test double with the
1112    // test-private `FlashDeviceMemory` in device-envoy-core's flash_block.rs tests.
1113    fn new() -> Self {
1114        Self {
1115            bytes: [FLASH_ERASED_BYTE; FLASH_BLOCK_SIZE],
1116        }
1117    }
1118
1119    fn checked_range(&self, offset: u32, len: usize) -> Range<usize> {
1120        let start = usize::try_from(offset).expect("flash offset must fit in usize");
1121        let end = start
1122            .checked_add(len)
1123            .expect("flash range must fit in usize");
1124        assert!(
1125            end <= FLASH_BLOCK_SIZE,
1126            "flash range must stay in the block"
1127        );
1128        start..end
1129    }
1130
1131    fn write_raw_bytes(&mut self, bytes: &[u8]) {
1132        self.bytes.fill(FLASH_ERASED_BYTE);
1133        let len = bytes.len().min(FLASH_BLOCK_SIZE);
1134        self.bytes[..len].copy_from_slice(&bytes[..len]);
1135    }
1136}
1137
1138#[cfg(test)]
1139impl FlashDevice for FlashDeviceMemory {
1140    type Error = Infallible;
1141
1142    fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
1143        let checked_range = self.checked_range(offset, bytes.len());
1144        bytes.copy_from_slice(&self.bytes[checked_range]);
1145        Ok(())
1146    }
1147
1148    fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
1149        let checked_range = self.checked_range(offset, bytes.len());
1150        self.bytes[checked_range].copy_from_slice(bytes);
1151        Ok(())
1152    }
1153
1154    fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
1155        let len = usize::try_from(to.saturating_sub(from)).expect("flash erase length fits usize");
1156        let checked_range = self.checked_range(from, len);
1157        self.bytes[checked_range].fill(FLASH_ERASED_BYTE);
1158        Ok(())
1159    }
1160}
1161
1162impl ButtonMemory {
1163    /// Construct a button test double with no frame scheduling.
1164    /// See the example on [`ButtonMemory`].
1165    #[must_use]
1166    pub fn new() -> Self {
1167        Self {
1168            pressed: false,
1169            pressed_frames: Vec::new(),
1170            frame_clock: None,
1171        }
1172    }
1173
1174    #[must_use]
1175    pub(crate) fn with_frame_clock(frame_clock: FrameClockMemory) -> Self {
1176        Self {
1177            pressed: false,
1178            pressed_frames: Vec::new(),
1179            frame_clock: Some(frame_clock),
1180        }
1181    }
1182
1183    /// Set the button's default pressed state.
1184    /// See the example on [`ButtonMemory`].
1185    pub fn set_pressed(&mut self, pressed: bool) {
1186        self.pressed = pressed;
1187    }
1188
1189    /// Override the pressed state for one specific flushed frame index.
1190    ///
1191    /// The [`CydMemory` example](CydMemory) demonstrates scheduled state taking
1192    /// effect after a frame flush.
1193    pub fn set_pressed_for_frame(&mut self, frame_index: usize, pressed: bool) {
1194        if let Some(existing_state) = self
1195            .pressed_frames
1196            .iter_mut()
1197            .find(|(existing_frame_index, _pressed_state)| *existing_frame_index == frame_index)
1198        {
1199            existing_state.1 = pressed;
1200            return;
1201        }
1202        self.pressed_frames.push((frame_index, pressed));
1203    }
1204
1205    fn current_pressed_state(&self) -> bool {
1206        let Some(frame_clock) = &self.frame_clock else {
1207            return self.pressed;
1208        };
1209        let frame_index = frame_clock.frame_index();
1210        self.pressed_frames
1211            .iter()
1212            .find_map(|(pressed_frame_index, pressed)| {
1213                (*pressed_frame_index == frame_index).then_some(*pressed)
1214            })
1215            .unwrap_or(self.pressed)
1216    }
1217}
1218
1219impl Default for ButtonMemory {
1220    fn default() -> Self {
1221        Self::new()
1222    }
1223}
1224
1225impl __ButtonMonitor for ButtonMemory {
1226    fn is_pressed_raw(&self) -> bool {
1227        self.current_pressed_state()
1228    }
1229
1230    async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
1231}
1232
1233impl Button for ButtonMemory {}
1234
1235fn fill_rectangle_in_framebuffer(
1236    framebuffer: &mut [u16],
1237    screen_size: Size,
1238    rectangle: Rectangle,
1239    color: u16,
1240) {
1241    let clipped_rectangle = rectangle.intersection(&Rectangle::new(Point::zero(), screen_size));
1242    if clipped_rectangle.size.width == 0 || clipped_rectangle.size.height == 0 {
1243        return;
1244    }
1245    let stride = screen_size.width as usize;
1246    for position_y in clipped_rectangle.top_left.y
1247        ..clipped_rectangle.top_left.y + clipped_rectangle.size.height as i32
1248    {
1249        for position_x in clipped_rectangle.top_left.x
1250            ..clipped_rectangle.top_left.x + clipped_rectangle.size.width as i32
1251        {
1252            let index = position_y as usize * stride + position_x as usize;
1253            framebuffer[index] = color;
1254        }
1255    }
1256}
1257
1258fn fill_contiguous_in_framebuffer<I>(
1259    framebuffer: &mut [u16],
1260    screen_size: Size,
1261    rectangle: Rectangle,
1262    pixels: I,
1263) where
1264    I: IntoIterator<Item = u16>,
1265{
1266    if rectangle.size.width == 0 || rectangle.size.height == 0 {
1267        return;
1268    }
1269    let stride = screen_size.width as usize;
1270    for (pixel_index, pixel) in pixels.into_iter().enumerate() {
1271        let local_x = pixel_index % rectangle.size.width as usize;
1272        let local_y = pixel_index / rectangle.size.width as usize;
1273        if local_y >= rectangle.size.height as usize {
1274            break;
1275        }
1276        let position_x = rectangle.top_left.x + local_x as i32;
1277        let position_y = rectangle.top_left.y + local_y as i32;
1278        if position_x < 0
1279            || position_y < 0
1280            || position_x >= screen_size.width as i32
1281            || position_y >= screen_size.height as i32
1282        {
1283            continue;
1284        }
1285        framebuffer[position_y as usize * stride + position_x as usize] = pixel;
1286    }
1287}
1288
1289fn blit_frame_to_screen(
1290    framebuffer: &mut [u16],
1291    screen_size: Size,
1292    rectangle: Rectangle,
1293    pixels: &[u16],
1294) {
1295    fill_contiguous_in_framebuffer(framebuffer, screen_size, rectangle, pixels.iter().copied());
1296}
1297
1298#[cfg(test)]
1299mod tests {
1300    use super::{
1301        ButtonMemory, CydMemory, CydTouchMemory, Error, FlashBlockMemory, MIN_SAMPLES_PER_POINT,
1302        SAMPLES_DISCARDED_AFTER_DOWN,
1303    };
1304    use crate::cyd::touch::driver::{
1305        CAPTURE_ACK_FRAME_COUNT, MAX_RAW_EVENTS_PER_FRAME, REJECTED_FRAME_COUNT,
1306        VERIFY_TIMEOUT_FRAMES,
1307    };
1308    use crate::cyd::{
1309        Cyd, CydDisplay, CydTouch,
1310        backend::{
1311            CalibrationConfig, Error as CalibrationError, RawTouchEvent, TouchUncalibrated,
1312            ensure_calibration,
1313        },
1314        display::{CydFrame, Orientation},
1315        touch::{
1316            RawPoint, TouchEvent,
1317            calibration::{
1318                CalibrationCorner, VERIFY_HIT_RADIUS_PIXELS, calibration_corner_center,
1319                calibration_verify_target_center, distort_demo_screen_to_raw,
1320            },
1321        },
1322    };
1323    use crate::flash_block::FlashBlock;
1324    use embedded_graphics::{
1325        Pixel,
1326        mono_font::ascii::FONT_9X15_BOLD,
1327        pixelcolor::{IntoStorage, Rgb565, Rgb888, WebColors},
1328        prelude::{DrawTarget, Point, Size},
1329        primitives::Rectangle,
1330    };
1331    use futures_executor::block_on;
1332    use serde::{Deserialize, Serialize};
1333
1334    #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1335    struct DemoValue {
1336        count: u16,
1337    }
1338
1339    fn test_cyd_memory() -> CydMemory {
1340        CydMemory::new(
1341            Size::new(320, 240),
1342            Rgb888::CSS_BLACK,
1343            Rgb888::CSS_WHITE,
1344            &FONT_9X15_BOLD,
1345        )
1346    }
1347
1348    #[test]
1349    fn orientation_is_preserved_by_oriented_memory() {
1350        for orientation in [
1351            crate::cyd::display::Orientation::Landscape,
1352            crate::cyd::display::Orientation::Portrait,
1353            crate::cyd::display::Orientation::LandscapeInverted,
1354            crate::cyd::display::Orientation::PortraitInverted,
1355        ] {
1356            let memory_cyd = CydMemory::new_with_orientation(
1357                orientation,
1358                Rgb888::CSS_BLACK,
1359                Rgb888::CSS_WHITE,
1360                &FONT_9X15_BOLD,
1361            );
1362            assert_eq!(memory_cyd.orientation(), orientation);
1363        }
1364    }
1365
1366    fn read_next_raw_touch_event(memory_cyd: &CydMemory) -> Result<Option<RawTouchEvent>, Error> {
1367        let (_display, mut touch) = memory_cyd.parts_uncalibrated();
1368        touch.read_raw_touch_event()
1369    }
1370
1371    fn run_ensure_calibration(
1372        memory_cyd: &CydMemory,
1373        memory_flash_block: &mut FlashBlockMemory,
1374        memory_button: &mut ButtonMemory,
1375        confirmed_message: Option<&str>,
1376    ) -> Result<CydTouchMemory, CalibrationError<Error, <FlashBlockMemory as FlashBlock>::Error>>
1377    {
1378        let (mut display, touch) = memory_cyd.parts_uncalibrated();
1379        block_on(ensure_calibration(
1380            &mut display,
1381            touch,
1382            memory_flash_block,
1383            memory_button,
1384            confirmed_message,
1385            memory_cyd.orientation(),
1386        ))
1387    }
1388
1389    #[test]
1390    fn fresh_frame_starts_cleared_to_background() {
1391        let memory_cyd = test_cyd_memory();
1392        let mut display = memory_cyd.display();
1393        let frame = display.frame_mut(Rectangle::new(Point::new(3, 4), Size::new(2, 2)));
1394        assert_eq!(frame.pixels, &[Rgb565::CSS_BLACK.into_storage(); 4]);
1395    }
1396
1397    #[test]
1398    fn short_fill_contiguous_iterator_changes_only_supplied_pixels() {
1399        let memory_cyd = test_cyd_memory();
1400        let rectangle = Rectangle::new(Point::new(2, 3), Size::new(2, 2));
1401        {
1402            let mut display = memory_cyd.display();
1403            display
1404                .fill_contiguous(rectangle, [Rgb565::CSS_RED, Rgb565::CSS_GREEN])
1405                .expect("memory streaming should succeed");
1406        }
1407
1408        assert_eq!(memory_cyd.pixel(2, 3), Rgb565::CSS_RED);
1409        assert_eq!(memory_cyd.pixel(3, 3), Rgb565::CSS_GREEN);
1410        assert_eq!(memory_cyd.pixel(2, 4), Rgb565::CSS_BLACK);
1411        assert_eq!(memory_cyd.pixel(3, 4), Rgb565::CSS_BLACK);
1412    }
1413
1414    #[test]
1415    fn overlong_fill_contiguous_iterator_ignores_pixels_beyond_rectangle() {
1416        let memory_cyd = test_cyd_memory();
1417        let rectangle = Rectangle::new(Point::new(2, 3), Size::new(2, 2));
1418        {
1419            let mut display = memory_cyd.display();
1420            display
1421                .fill_contiguous(
1422                    rectangle,
1423                    [
1424                        Rgb565::CSS_RED,
1425                        Rgb565::CSS_GREEN,
1426                        Rgb565::CSS_BLUE,
1427                        Rgb565::CSS_WHITE,
1428                        Rgb565::CSS_YELLOW,
1429                    ],
1430                )
1431                .expect("memory streaming should succeed");
1432        }
1433
1434        assert_eq!(memory_cyd.pixel(2, 3), Rgb565::CSS_RED);
1435        assert_eq!(memory_cyd.pixel(3, 3), Rgb565::CSS_GREEN);
1436        assert_eq!(memory_cyd.pixel(2, 4), Rgb565::CSS_BLUE);
1437        assert_eq!(memory_cyd.pixel(3, 4), Rgb565::CSS_WHITE);
1438        assert_eq!(memory_cyd.pixel(4, 3), Rgb565::CSS_BLACK);
1439    }
1440
1441    #[test]
1442    fn draw_target_pixel_flushes_to_screen_coordinate() {
1443        let memory_cyd = test_cyd_memory();
1444        {
1445            let mut display = memory_cyd.display();
1446            let mut frame = display.frame_mut(Rectangle::new(Point::new(10, 20), Size::new(4, 3)));
1447            frame
1448                .draw_iter([Pixel(Point::new(11, 21), Rgb565::CSS_RED)])
1449                .expect("drawing into memory frame should succeed");
1450            block_on(frame.flush()).expect("flush should succeed");
1451        }
1452        assert_eq!(memory_cyd.pixel(11, 21), Rgb565::CSS_RED);
1453        assert_eq!(
1454            memory_cyd.last_flush_rectangle(),
1455            Some(Rectangle::new(Point::new(10, 20), Size::new(4, 3)))
1456        );
1457    }
1458
1459    #[test]
1460    fn fill_rectangle_clips_to_screen_edges() {
1461        let memory_cyd = CydMemory::new(
1462            Size::new(4, 4),
1463            Rgb888::CSS_BLACK,
1464            Rgb888::CSS_WHITE,
1465            &FONT_9X15_BOLD,
1466        );
1467        {
1468            let mut display = memory_cyd.display();
1469            display
1470                .fill_rectangle(
1471                    Rectangle::new(Point::new(-1, -1), Size::new(3, 3)),
1472                    Rgb565::CSS_GREEN,
1473                )
1474                .expect("fill_rectangle should succeed");
1475            display
1476                .fill_rectangle(
1477                    Rectangle::new(Point::new(10, 10), Size::new(2, 2)),
1478                    Rgb565::CSS_RED,
1479                )
1480                .expect("off-screen fill_rectangle should stay a no-op");
1481        }
1482        assert_eq!(memory_cyd.pixel(0, 0), Rgb565::CSS_GREEN);
1483        assert_eq!(memory_cyd.pixel(1, 1), Rgb565::CSS_GREEN);
1484        assert_eq!(memory_cyd.pixel(3, 3), Rgb565::CSS_BLACK);
1485    }
1486
1487    #[test]
1488    fn raw_touch_frames_drain_then_advance_after_flush() {
1489        let mut memory_cyd = test_cyd_memory();
1490        let first_frame = [
1491            RawTouchEvent::Down { raw_x: 1, raw_y: 2 },
1492            RawTouchEvent::Up,
1493        ];
1494        let second_frame = [RawTouchEvent::Down { raw_x: 3, raw_y: 4 }];
1495        memory_cyd.script_raw_frames(&[&first_frame, &second_frame]);
1496
1497        assert_eq!(
1498            read_next_raw_touch_event(&memory_cyd).expect("read should succeed"),
1499            Some(RawTouchEvent::Down { raw_x: 1, raw_y: 2 })
1500        );
1501        assert_eq!(
1502            read_next_raw_touch_event(&memory_cyd).expect("read should succeed"),
1503            Some(RawTouchEvent::Up)
1504        );
1505        assert_eq!(
1506            read_next_raw_touch_event(&memory_cyd).expect("read should succeed"),
1507            None
1508        );
1509
1510        {
1511            let mut display = memory_cyd.display();
1512            let mut frame = display.full_frame_mut();
1513            block_on(frame.flush()).expect("flush should succeed");
1514        }
1515
1516        assert_eq!(memory_cyd.flush_count(), 1);
1517        assert_eq!(
1518            read_next_raw_touch_event(&memory_cyd).expect("read should succeed"),
1519            Some(RawTouchEvent::Down { raw_x: 3, raw_y: 4 })
1520        );
1521    }
1522
1523    #[test]
1524    fn flush_budget_returns_out_of_frames() {
1525        let mut memory_cyd = test_cyd_memory();
1526        memory_cyd.set_frame_budget(1);
1527        {
1528            let mut display = memory_cyd.display();
1529            let mut frame = display.full_frame_mut();
1530            block_on(frame.flush()).expect("first flush should succeed");
1531        }
1532        {
1533            let mut display = memory_cyd.display();
1534            let mut frame = display.full_frame_mut();
1535            let error = block_on(frame.flush()).expect_err("second flush should hit frame budget");
1536            assert_eq!(error, Error::OutOfFrames);
1537        }
1538        assert_eq!(memory_cyd.flush_count(), 1);
1539    }
1540
1541    #[test]
1542    fn memory_flash_block_round_trips_and_handles_corruption() {
1543        let mut memory_flash_block = FlashBlockMemory::new();
1544        memory_flash_block
1545            .save(&DemoValue { count: 7 })
1546            .expect("save should succeed");
1547        assert_eq!(
1548            memory_flash_block
1549                .load::<DemoValue>()
1550                .expect("load should succeed"),
1551            Some(DemoValue { count: 7 })
1552        );
1553
1554        let mut corrupt_flash_block = FlashBlockMemory::with_raw_bytes(&[1, 2, 3, 4]);
1555        assert_eq!(
1556            corrupt_flash_block
1557                .load::<DemoValue>()
1558                .expect("corrupt load should degrade to None"),
1559            None
1560        );
1561
1562        memory_flash_block.clear().expect("clear should succeed");
1563        assert_eq!(
1564            memory_flash_block
1565                .load::<DemoValue>()
1566                .expect("load should succeed"),
1567            None
1568        );
1569    }
1570
1571    #[test]
1572    fn ensure_calibration_happy_path_saves_predictable_config() {
1573        let mut memory_cyd = test_cyd_memory();
1574        let mut memory_flash_block = FlashBlockMemory::new();
1575        let mut memory_button = memory_cyd.button_memory();
1576        let raw_points = script_happy_path(&mut memory_cyd);
1577
1578        let _touch = run_ensure_calibration(
1579            &memory_cyd,
1580            &mut memory_flash_block,
1581            &mut memory_button,
1582            Some("saved"),
1583        )
1584        .expect("happy-path calibration should succeed");
1585
1586        assert_eq!(memory_flash_block.save_count(), 1);
1587
1588        let saved_config = memory_flash_block
1589            .load::<CalibrationConfig>()
1590            .expect("saved config should deserialize")
1591            .expect("saved config should exist");
1592
1593        for (raw_point, calibration_corner) in raw_points.into_iter().zip([
1594            CalibrationCorner::UpperLeft,
1595            CalibrationCorner::UpperRight,
1596            CalibrationCorner::LowerRight,
1597            CalibrationCorner::LowerLeft,
1598        ]) {
1599            let expected_screen_point = calibration_corner_center(calibration_corner);
1600            let (mapped_x, mapped_y) = saved_config.map_raw_to_screen(raw_point.x, raw_point.y);
1601            assert!(
1602                (mapped_x - expected_screen_point.x as f32).abs() <= 1.0,
1603                "mapped_x={mapped_x} expected_x={}",
1604                expected_screen_point.x
1605            );
1606            assert!(
1607                (mapped_y - expected_screen_point.y as f32).abs() <= 1.0,
1608                "mapped_y={mapped_y} expected_y={}",
1609                expected_screen_point.y
1610            );
1611        }
1612        assert!(memory_cyd.flush_count() > 0);
1613        // The confirmation message is the only buffered flush per redraw;
1614        // target/dot geometry streams buffer-free via `draw_items` and
1615        // doesn't touch `last_flush_rectangle`. See `CALIBRATION_TEXT_RECTANGLE`.
1616        assert_eq!(
1617            memory_cyd.last_flush_rectangle(),
1618            Some(Rectangle::new(Point::new(0, 220), Size::new(320, 20)))
1619        );
1620    }
1621
1622    #[test]
1623    fn ensure_calibration_uses_preloaded_flash_without_flushing() {
1624        let mut memory_cyd = test_cyd_memory();
1625        let saved_config = CalibrationConfig::new(1.0, 0.0, 2.0, 0.0, 1.0, 3.0);
1626        memory_cyd.push_raw_touch_event(RawTouchEvent::Down { raw_x: 7, raw_y: 9 });
1627        let mut memory_flash_block = FlashBlockMemory::with_value(&saved_config);
1628        let mut memory_button = memory_cyd.button_memory();
1629
1630        let _touch = run_ensure_calibration(
1631            &memory_cyd,
1632            &mut memory_flash_block,
1633            &mut memory_button,
1634            None,
1635        )
1636        .expect("preloaded calibration should load");
1637
1638        assert_eq!(memory_cyd.flush_count(), 0);
1639        let (_display, mut touch) = memory_cyd.parts_uncalibrated();
1640        assert_eq!(
1641            touch
1642                .read_raw_touch_event()
1643                .expect("touch read should succeed"),
1644            Some(RawTouchEvent::Down { raw_x: 7, raw_y: 9 })
1645        );
1646    }
1647
1648    #[test]
1649    fn preloaded_calibration_preserves_already_oriented_memory_events() {
1650        for orientation in [
1651            Orientation::Landscape,
1652            Orientation::Portrait,
1653            Orientation::LandscapeInverted,
1654            Orientation::PortraitInverted,
1655        ] {
1656            let mut memory_cyd = CydMemory::new_with_orientation(
1657                orientation,
1658                Rgb888::CSS_BLACK,
1659                Rgb888::CSS_WHITE,
1660                &FONT_9X15_BOLD,
1661            );
1662            let point = Point::new(
1663                orientation.width() as i32 - 1,
1664                orientation.height() as i32 - 1,
1665            );
1666            memory_cyd.push_touch_event(TouchEvent::Down { point });
1667            let saved_config = super::identity_calibration_config();
1668            let mut memory_flash_block = FlashBlockMemory::with_value(&saved_config);
1669            let mut memory_button = memory_cyd.button_memory();
1670            let mut touch = run_ensure_calibration(
1671                &memory_cyd,
1672                &mut memory_flash_block,
1673                &mut memory_button,
1674                None,
1675            )
1676            .expect("preloaded calibration should load");
1677
1678            assert!(matches!(
1679                touch.try_read(),
1680                Ok(Some(TouchEvent::Down { point: actual_point }))
1681                    if actual_point == point
1682            ));
1683        }
1684    }
1685
1686    #[test]
1687    fn ensure_calibration_corrupt_flash_reruns_and_overwrites() {
1688        let mut memory_cyd = test_cyd_memory();
1689        let mut memory_flash_block = FlashBlockMemory::with_raw_bytes(&[1, 2, 3, 4]);
1690        let mut memory_button = memory_cyd.button_memory();
1691        script_happy_path(&mut memory_cyd);
1692
1693        let _touch = run_ensure_calibration(
1694            &memory_cyd,
1695            &mut memory_flash_block,
1696            &mut memory_button,
1697            None,
1698        )
1699        .expect("corrupt flash should fall back to calibration");
1700
1701        assert_eq!(memory_flash_block.save_count(), 1);
1702        assert!(
1703            memory_flash_block
1704                .load::<CalibrationConfig>()
1705                .expect("load should succeed")
1706                .is_some()
1707        );
1708    }
1709
1710    #[test]
1711    fn ensure_calibration_paces_with_one_flush_per_iteration() {
1712        let mut memory_cyd = test_cyd_memory();
1713        memory_cyd.set_frame_budget(3);
1714        let mut memory_flash_block = FlashBlockMemory::new();
1715        let mut memory_button = memory_cyd.button_memory();
1716
1717        let error = run_ensure_calibration(
1718            &memory_cyd,
1719            &mut memory_flash_block,
1720            &mut memory_button,
1721            None,
1722        )
1723        .expect_err("empty input should stop at the frame budget");
1724
1725        assert!(matches!(
1726            error,
1727            CalibrationError::Device(Error::OutOfFrames)
1728        ));
1729        assert_eq!(memory_cyd.flush_count(), 3);
1730    }
1731
1732    #[test]
1733    fn ensure_calibration_drains_a_full_tap_in_one_frame() {
1734        let mut memory_cyd = test_cyd_memory();
1735        memory_cyd.set_frame_budget(1);
1736        let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1737        memory_cyd.script_raw_frames_owned(vec![tap_events(upper_left_raw_point)]);
1738        let mut memory_flash_block = FlashBlockMemory::new();
1739        let mut memory_button = memory_cyd.button_memory();
1740
1741        let error = run_ensure_calibration(
1742            &memory_cyd,
1743            &mut memory_flash_block,
1744            &mut memory_button,
1745            None,
1746        )
1747        .expect_err("single-frame budget should stop after the first drawn frame");
1748
1749        assert!(matches!(
1750            error,
1751            CalibrationError::Device(Error::OutOfFrames)
1752        ));
1753        let upper_left_center = calibration_corner_center(CalibrationCorner::UpperLeft);
1754        let upper_right_center = calibration_corner_center(CalibrationCorner::UpperRight);
1755        assert_eq!(
1756            memory_cyd.pixel(upper_left_center.x as usize, upper_left_center.y as usize),
1757            Rgb565::CSS_WHITE
1758        );
1759        assert_eq!(
1760            memory_cyd.pixel(upper_right_center.x as usize, upper_right_center.y as usize),
1761            Rgb565::CSS_WHITE
1762        );
1763        assert_eq!(memory_cyd.pixel(160, 120), Rgb565::CSS_BLACK);
1764    }
1765
1766    #[test]
1767    fn ensure_calibration_verify_timeout_restarts_and_then_succeeds() {
1768        let mut memory_cyd = test_cyd_memory();
1769        let mut frames = happy_path_frames();
1770        frames.truncate(frames.len() - 1);
1771        frames.extend((0..verify_timeout_extra_idle_frames()).map(|_| Vec::new()));
1772        frames.extend((0..rejected_restart_idle_frames()).map(|_| Vec::new()));
1773        frames.extend(happy_path_frames());
1774        memory_cyd.script_raw_frames_owned(frames);
1775
1776        let mut memory_flash_block = FlashBlockMemory::new();
1777        let mut memory_button = memory_cyd.button_memory();
1778
1779        let _touch = run_ensure_calibration(
1780            &memory_cyd,
1781            &mut memory_flash_block,
1782            &mut memory_button,
1783            None,
1784        )
1785        .expect("flow should restart after verify timeout and then save");
1786
1787        assert_eq!(memory_flash_block.save_count(), 1);
1788    }
1789
1790    #[test]
1791    fn ensure_calibration_dropout_does_not_leak_corner_two_into_corner_three() {
1792        let mut memory_cyd = test_cyd_memory();
1793        let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1794        let upper_right_raw_point = raw_point_for_corner(CalibrationCorner::UpperRight);
1795        let lower_right_raw_point = raw_point_for_corner(CalibrationCorner::LowerRight);
1796        let lower_left_raw_point = raw_point_for_corner(CalibrationCorner::LowerLeft);
1797        let verify_raw_point = raw_point_for_verify_target();
1798        let mut frames = vec![tap_events(upper_left_raw_point)];
1799        append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
1800        frames.push(dropout_tap_events(upper_right_raw_point));
1801        append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
1802        frames.push(tap_events(lower_right_raw_point));
1803        append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
1804        frames.push(tap_events(lower_left_raw_point));
1805        frames.push(tap_events(verify_raw_point));
1806        memory_cyd.script_raw_frames_owned(frames);
1807
1808        let mut memory_flash_block = FlashBlockMemory::new();
1809        let mut memory_button = memory_cyd.button_memory();
1810        let _touch = run_ensure_calibration(
1811            &memory_cyd,
1812            &mut memory_flash_block,
1813            &mut memory_button,
1814            None,
1815        )
1816        .expect("dropout sequence should still save a calibration");
1817
1818        let calibration_config = memory_flash_block
1819            .load::<CalibrationConfig>()
1820            .unwrap()
1821            .unwrap();
1822        assert_maps_near_corner(
1823            calibration_config,
1824            lower_right_raw_point,
1825            CalibrationCorner::LowerRight,
1826        );
1827    }
1828
1829    #[test]
1830    fn ensure_calibration_lift_off_drift_keeps_captured_point_near_stable_raw_point() {
1831        let mut memory_cyd = test_cyd_memory();
1832        let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1833        let drifted_raw_point = RawPoint {
1834            x: upper_left_raw_point.x + 400,
1835            y: upper_left_raw_point.y + 400,
1836        };
1837        let upper_right_raw_point = raw_point_for_corner(CalibrationCorner::UpperRight);
1838        let lower_right_raw_point = raw_point_for_corner(CalibrationCorner::LowerRight);
1839        let lower_left_raw_point = raw_point_for_corner(CalibrationCorner::LowerLeft);
1840        let verify_raw_point = raw_point_for_verify_target();
1841        let mut frames = vec![long_press_with_lift_off_drift_frame(
1842            upper_left_raw_point,
1843            drifted_raw_point,
1844        )];
1845        append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
1846        frames.extend(calibration_attempt_frames(&[
1847            upper_right_raw_point,
1848            lower_right_raw_point,
1849            lower_left_raw_point,
1850            verify_raw_point,
1851        ]));
1852        memory_cyd.script_raw_frames_owned(frames);
1853
1854        let mut memory_flash_block = FlashBlockMemory::new();
1855        let mut memory_button = memory_cyd.button_memory();
1856        let _touch = run_ensure_calibration(
1857            &memory_cyd,
1858            &mut memory_flash_block,
1859            &mut memory_button,
1860            None,
1861        )
1862        .expect("lift-off drift sequence should still save a calibration");
1863
1864        let calibration_config = memory_flash_block
1865            .load::<CalibrationConfig>()
1866            .unwrap()
1867            .unwrap();
1868        assert_maps_near_corner(
1869            calibration_config,
1870            upper_left_raw_point,
1871            CalibrationCorner::UpperLeft,
1872        );
1873    }
1874
1875    #[test]
1876    fn ensure_calibration_rejected_solve_restarts_and_then_saves_honest_script() {
1877        let mut memory_cyd = test_cyd_memory();
1878        let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1879        let lower_right_raw_point = raw_point_for_corner(CalibrationCorner::LowerRight);
1880        let lower_left_raw_point = raw_point_for_corner(CalibrationCorner::LowerLeft);
1881        let mut frames = calibration_attempt_frames(&[
1882            upper_left_raw_point,
1883            upper_left_raw_point,
1884            lower_right_raw_point,
1885            lower_left_raw_point,
1886            raw_point_for_verify_target(),
1887        ]);
1888        append_idle_frames(&mut frames, rejected_restart_idle_frames());
1889        frames.extend(happy_path_frames());
1890        memory_cyd.script_raw_frames_owned(frames);
1891
1892        let mut memory_flash_block = FlashBlockMemory::new();
1893        let mut memory_button = memory_cyd.button_memory();
1894        let _touch = run_ensure_calibration(
1895            &memory_cyd,
1896            &mut memory_flash_block,
1897            &mut memory_button,
1898            None,
1899        )
1900        .expect("rejected solve should restart and then save");
1901
1902        let calibration_config = memory_flash_block
1903            .load::<CalibrationConfig>()
1904            .unwrap()
1905            .unwrap();
1906        assert_eq!(memory_flash_block.save_count(), 1);
1907        assert_maps_near_corner(
1908            calibration_config,
1909            raw_point_for_corner(CalibrationCorner::UpperRight),
1910            CalibrationCorner::UpperRight,
1911        );
1912    }
1913
1914    #[test]
1915    fn ensure_calibration_verify_miss_restarts_without_saving_candidate() {
1916        let mut memory_cyd = test_cyd_memory();
1917        let verify_target_center = calibration_verify_target_center();
1918        let verify_miss_screen_x =
1919            verify_target_center.x + VERIFY_HIT_RADIUS_PIXELS.ceil() as i32 + 10;
1920        let verify_miss_raw_point =
1921            distort_demo_screen_to_raw(verify_miss_screen_x as f32, verify_target_center.y as f32);
1922        let mut frames = calibration_attempt_frames(&[
1923            raw_point_for_corner(CalibrationCorner::UpperLeft),
1924            raw_point_for_corner(CalibrationCorner::UpperRight),
1925            raw_point_for_corner(CalibrationCorner::LowerRight),
1926            raw_point_for_corner(CalibrationCorner::LowerLeft),
1927            verify_miss_raw_point,
1928        ]);
1929        append_idle_frames(&mut frames, rejected_restart_idle_frames());
1930        frames.extend(happy_path_frames());
1931        memory_cyd.script_raw_frames_owned(frames);
1932
1933        let mut memory_flash_block = FlashBlockMemory::new();
1934        let mut memory_button = memory_cyd.button_memory();
1935        let _touch = run_ensure_calibration(
1936            &memory_cyd,
1937            &mut memory_flash_block,
1938            &mut memory_button,
1939            None,
1940        )
1941        .expect("verify miss should restart and then save");
1942
1943        assert_eq!(memory_flash_block.save_count(), 1);
1944    }
1945
1946    #[test]
1947    fn ensure_calibration_recalibration_button_restarts_mid_flow() {
1948        let mut memory_cyd = test_cyd_memory();
1949        let mut frames = vec![tap_events(raw_point_for_corner(
1950            CalibrationCorner::UpperLeft,
1951        ))];
1952        append_idle_frames(&mut frames, 2);
1953        frames.extend(happy_path_frames());
1954        memory_cyd.script_raw_frames_owned(frames);
1955
1956        let mut memory_flash_block = FlashBlockMemory::new();
1957        let mut memory_button = memory_cyd.button_memory();
1958        memory_button.set_pressed_for_frame(2, true);
1959        let _touch = run_ensure_calibration(
1960            &memory_cyd,
1961            &mut memory_flash_block,
1962            &mut memory_button,
1963            None,
1964        )
1965        .expect("button-triggered recalibration should restart and then save");
1966
1967        let calibration_config = memory_flash_block
1968            .load::<CalibrationConfig>()
1969            .unwrap()
1970            .unwrap();
1971        assert_eq!(memory_flash_block.save_count(), 1);
1972        assert_maps_near_corner(
1973            calibration_config,
1974            raw_point_for_corner(CalibrationCorner::UpperLeft),
1975            CalibrationCorner::UpperLeft,
1976        );
1977    }
1978
1979    #[test]
1980    fn ensure_calibration_drain_cap_flushes_and_preserves_leftovers_during_hold() {
1981        let mut memory_cyd = test_cyd_memory();
1982        memory_cyd.set_frame_budget(2);
1983        let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1984        let mut oversized_hold_frame = Vec::new();
1985        oversized_hold_frame.push(RawTouchEvent::Down {
1986            raw_x: upper_left_raw_point.x,
1987            raw_y: upper_left_raw_point.y,
1988        });
1989        for _raw_event_index in 0..MAX_RAW_EVENTS_PER_FRAME.saturating_sub(1) {
1990            oversized_hold_frame.push(RawTouchEvent::Move {
1991                raw_x: upper_left_raw_point.x,
1992                raw_y: upper_left_raw_point.y,
1993            });
1994        }
1995        oversized_hold_frame.push(RawTouchEvent::Up);
1996        memory_cyd.script_raw_frames_owned(vec![oversized_hold_frame]);
1997
1998        let mut memory_flash_block = FlashBlockMemory::new();
1999        let mut memory_button = memory_cyd.button_memory();
2000        let error = run_ensure_calibration(
2001            &memory_cyd,
2002            &mut memory_flash_block,
2003            &mut memory_button,
2004            None,
2005        )
2006        .expect_err("oversized hold should stop at the frame budget");
2007
2008        assert!(matches!(
2009            error,
2010            CalibrationError::Device(Error::OutOfFrames)
2011        ));
2012        assert_eq!(memory_cyd.flush_count(), 2);
2013        let upper_left_center = calibration_corner_center(CalibrationCorner::UpperLeft);
2014        let upper_right_center = calibration_corner_center(CalibrationCorner::UpperRight);
2015        assert_eq!(
2016            memory_cyd.pixel(upper_left_center.x as usize, upper_left_center.y as usize),
2017            Rgb565::CSS_WHITE
2018        );
2019        assert_eq!(
2020            memory_cyd.pixel(upper_right_center.x as usize, upper_right_center.y as usize),
2021            Rgb565::CSS_WHITE
2022        );
2023        assert_eq!(
2024            read_next_raw_touch_event(&memory_cyd)
2025                .expect("the oversized frame should be fully drained by the second iteration"),
2026            None
2027        );
2028    }
2029
2030    fn script_happy_path(memory_cyd: &mut CydMemory) -> [RawPoint; 4] {
2031        memory_cyd.script_raw_frames_owned(happy_path_frames());
2032        [
2033            raw_point_for_corner(CalibrationCorner::UpperLeft),
2034            raw_point_for_corner(CalibrationCorner::UpperRight),
2035            raw_point_for_corner(CalibrationCorner::LowerRight),
2036            raw_point_for_corner(CalibrationCorner::LowerLeft),
2037        ]
2038    }
2039
2040    fn happy_path_frames() -> Vec<Vec<RawTouchEvent>> {
2041        calibration_attempt_frames(&[
2042            raw_point_for_corner(CalibrationCorner::UpperLeft),
2043            raw_point_for_corner(CalibrationCorner::UpperRight),
2044            raw_point_for_corner(CalibrationCorner::LowerRight),
2045            raw_point_for_corner(CalibrationCorner::LowerLeft),
2046            raw_point_for_verify_target(),
2047        ])
2048    }
2049
2050    fn raw_point_for_corner(calibration_corner: CalibrationCorner) -> RawPoint {
2051        let screen_point = calibration_corner_center(calibration_corner);
2052        distort_demo_screen_to_raw(screen_point.x as f32, screen_point.y as f32)
2053    }
2054
2055    fn tap_events(raw_point: RawPoint) -> Vec<RawTouchEvent> {
2056        let mut raw_touch_events = Vec::new();
2057        raw_touch_events.push(RawTouchEvent::Down {
2058            raw_x: raw_point.x,
2059            raw_y: raw_point.y,
2060        });
2061        for _discarded_sample_index in 0..SAMPLES_DISCARDED_AFTER_DOWN {
2062            raw_touch_events.push(RawTouchEvent::Move {
2063                raw_x: raw_point.x,
2064                raw_y: raw_point.y,
2065            });
2066        }
2067        for _usable_sample_index in 0..MIN_SAMPLES_PER_POINT {
2068            raw_touch_events.push(RawTouchEvent::Move {
2069                raw_x: raw_point.x,
2070                raw_y: raw_point.y,
2071            });
2072        }
2073        raw_touch_events.push(RawTouchEvent::Up);
2074        raw_touch_events
2075    }
2076
2077    fn dropout_tap_events(raw_point: RawPoint) -> Vec<RawTouchEvent> {
2078        let mut raw_touch_events = tap_events(raw_point);
2079        raw_touch_events.extend([
2080            RawTouchEvent::Down {
2081                raw_x: raw_point.x,
2082                raw_y: raw_point.y,
2083            },
2084            RawTouchEvent::Move {
2085                raw_x: raw_point.x,
2086                raw_y: raw_point.y,
2087            },
2088            RawTouchEvent::Up,
2089        ]);
2090        raw_touch_events
2091    }
2092
2093    fn long_press_with_lift_off_drift_frame(
2094        stable_raw_point: RawPoint,
2095        drifted_raw_point: RawPoint,
2096    ) -> Vec<RawTouchEvent> {
2097        let mut raw_touch_events = Vec::new();
2098        raw_touch_events.push(RawTouchEvent::Down {
2099            raw_x: stable_raw_point.x,
2100            raw_y: stable_raw_point.y,
2101        });
2102        for _stable_move_index in 0..2_004 {
2103            raw_touch_events.push(RawTouchEvent::Move {
2104                raw_x: stable_raw_point.x,
2105                raw_y: stable_raw_point.y,
2106            });
2107        }
2108        for _drifted_move_index in 0..3 {
2109            raw_touch_events.push(RawTouchEvent::Move {
2110                raw_x: drifted_raw_point.x,
2111                raw_y: drifted_raw_point.y,
2112            });
2113        }
2114        raw_touch_events.push(RawTouchEvent::Up);
2115        raw_touch_events
2116    }
2117
2118    fn calibration_attempt_frames(raw_points: &[RawPoint]) -> Vec<Vec<RawTouchEvent>> {
2119        let mut frames = Vec::new();
2120        for (tap_index, raw_point) in raw_points.iter().copied().enumerate() {
2121            frames.push(tap_events(raw_point));
2122            if tap_index + 2 < raw_points.len() {
2123                append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
2124            }
2125        }
2126        frames
2127    }
2128
2129    fn append_idle_frames(frames: &mut Vec<Vec<RawTouchEvent>>, idle_frame_count: usize) {
2130        frames.extend((0..idle_frame_count).map(|_| Vec::new()));
2131    }
2132
2133    fn raw_point_for_verify_target() -> RawPoint {
2134        let verify_center = calibration_verify_target_center();
2135        distort_demo_screen_to_raw(verify_center.x as f32, verify_center.y as f32)
2136    }
2137
2138    fn assert_maps_near_corner(
2139        calibration_config: CalibrationConfig,
2140        raw_point: RawPoint,
2141        calibration_corner: CalibrationCorner,
2142    ) {
2143        let expected_screen_point = calibration_corner_center(calibration_corner);
2144        let (mapped_x, mapped_y) = calibration_config.map_raw_to_screen(raw_point.x, raw_point.y);
2145        assert!(
2146            (mapped_x - expected_screen_point.x as f32).abs() <= 1.0,
2147            "mapped_x={mapped_x} expected_x={}",
2148            expected_screen_point.x
2149        );
2150        assert!(
2151            (mapped_y - expected_screen_point.y as f32).abs() <= 1.0,
2152            "mapped_y={mapped_y} expected_y={}",
2153            expected_screen_point.y
2154        );
2155    }
2156
2157    const fn capture_ack_extra_idle_frames() -> usize {
2158        // The tap frame may end with an immediate `None`, but that idle pass only
2159        // decrements the freshly-entered `ShowCaptured` state before drawing its
2160        // first acknowledgment screen. Tests still need a full
2161        // `CAPTURE_ACK_FRAME_COUNT` later idle frames before the next scripted tap
2162        // is guaranteed to run after the ack window.
2163        CAPTURE_ACK_FRAME_COUNT
2164    }
2165
2166    const fn rejected_restart_idle_frames() -> usize {
2167        REJECTED_FRAME_COUNT
2168    }
2169
2170    const fn verify_timeout_extra_idle_frames() -> usize {
2171        VERIFY_TIMEOUT_FRAMES.saturating_sub(1)
2172    }
2173}