diff --git a/app-core/src/lib.rs b/app-core/src/lib.rs index 6270194b..cea21607 100644 --- a/app-core/src/lib.rs +++ b/app-core/src/lib.rs @@ -10,7 +10,7 @@ pub mod buttons; /// The storage/display task's command loop, as sequences a host test can drive. pub mod storage_loop; -pub const SETTINGS_ITEMS: u8 = 7; +pub const SETTINGS_ITEMS: u8 = 8; pub const MAX_SD_CHAPTERS: usize = 128; pub const FIRST_SD_BOOK_ID: u32 = 2; @@ -135,6 +135,12 @@ pub enum FrontButtons { PagesLeft, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RefreshQuality { + Normal, + Fast, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RefreshPolicy { FastOnly, @@ -314,6 +320,7 @@ pub struct RenderRequest { /// sheet, relabel the key rail, and show the wait or its note. pub library_menu: LibraryMenu, pub refresh_policy: RefreshPolicy, + pub refresh_quality: RefreshQuality, pub font_size: FontSize, pub line_spacing: LineSpacing, pub font_weight: FontWeight, @@ -1322,6 +1329,7 @@ pub enum LibraryEvent { page_count: u32, reading_orientation: u8, refresh_policy: u8, + refresh_quality: u8, font_size: u8, line_spacing: u8, font_weight: u8, @@ -1801,6 +1809,7 @@ pub struct PersistedAppState { pub shell_orientation: u8, pub reading_orientation: u8, pub refresh_policy: u8, + pub refresh_quality: u8, pub font_size: u8, pub line_spacing: u8, pub font_weight: u8, @@ -1835,6 +1844,7 @@ pub struct ReaderState { pub orientation: DisplayOrientation, pub front_buttons: FrontButtons, pub refresh_policy: RefreshPolicy, + pub refresh_quality: RefreshQuality, pub font_size: FontSize, pub line_spacing: LineSpacing, pub font_weight: FontWeight, @@ -1890,6 +1900,7 @@ impl ReaderState { orientation: DisplayOrientation::PortraitButtonsLeft, front_buttons: FrontButtons::PagesRight, refresh_policy: RefreshPolicy::FullOnWake, + refresh_quality: RefreshQuality::Normal, font_size: FontSize::Medium, line_spacing: LineSpacing::Normal, font_weight: FontWeight::Normal, @@ -2383,6 +2394,7 @@ impl ReaderState { page_count, reading_orientation, refresh_policy, + refresh_quality, font_size, line_spacing, font_weight, @@ -2416,6 +2428,9 @@ impl ReaderState { if let Some(policy) = refresh_policy_from_u8(refresh_policy) { self.refresh_policy = policy; } + if let Some(quality) = refresh_quality_from_u8(refresh_quality) { + self.refresh_quality = quality; + } if let Some(size) = FontSize::from_u8(font_size) { self.font_size = size; } @@ -2499,6 +2514,7 @@ impl ReaderState { reading_sheet: self.reading_sheet, library_menu: self.library_menu, refresh_policy: self.refresh_policy, + refresh_quality: self.refresh_quality, font_size: self.font_size, line_spacing: self.line_spacing, font_weight: self.font_weight, @@ -2527,6 +2543,7 @@ impl ReaderState { shell_orientation: DisplayOrientation::PortraitButtonsLeft as u8, reading_orientation: self.orientation as u8, refresh_policy: self.refresh_policy as u8, + refresh_quality: self.refresh_quality as u8, font_size: self.font_size as u8, line_spacing: self.line_spacing as u8, font_weight: self.font_weight as u8, @@ -2658,6 +2675,14 @@ pub fn refresh_policy_from_u8(value: u8) -> Option { } } +pub fn refresh_quality_from_u8(value: u8) -> Option { + match value { + 0 => Some(RefreshQuality::Normal), + 1 => Some(RefreshQuality::Fast), + _ => None, + } +} + fn wrap_next(value: u16, len: u16) -> u16 { if value + 1 >= len { 0 @@ -2809,6 +2834,12 @@ fn apply_setting(mut state: ReaderState) -> ReaderState { FrontButtons::PagesLeft => FrontButtons::PagesRight, }; } + 7 => { + state.refresh_quality = match state.refresh_quality { + RefreshQuality::Normal => RefreshQuality::Fast, + RefreshQuality::Fast => RefreshQuality::Normal, + }; + } _ => {} } state @@ -3501,7 +3532,7 @@ mod tests { // 16 bytes and the planner's stored request four. Recorded because // `.bss` trades one-for-one against the main stack region on this // target, so a struct in a channel is never free. - assert_eq!(core::mem::size_of::(), 112); + assert_eq!(core::mem::size_of::(), 120); assert!( core::mem::size_of::() < core::mem::size_of::(), "the departing state has outgrown the credentials variant", @@ -4000,6 +4031,7 @@ mod tests { page_count: 0, reading_orientation: 0, refresh_policy: 0, + refresh_quality: 0, font_size: 0, line_spacing: 0, font_weight: 0, @@ -4846,6 +4878,7 @@ mod tests { page_count: 0, reading_orientation: DisplayOrientation::LandscapeButtonsBottom as u8, refresh_policy: RefreshPolicy::FullOnWake as u8, + refresh_quality: RefreshQuality::Normal as u8, font_size: FontSize::Medium as u8, line_spacing: LineSpacing::Normal as u8, font_weight: FontWeight::Normal as u8, @@ -4893,6 +4926,7 @@ mod tests { page_count: 0, reading_orientation: DisplayOrientation::LandscapeButtonsBottom as u8, refresh_policy: RefreshPolicy::FullOnWake as u8, + refresh_quality: RefreshQuality::Normal as u8, font_size: FontSize::Medium as u8, line_spacing: LineSpacing::Normal as u8, font_weight: FontWeight::Normal as u8, @@ -4968,9 +5002,23 @@ mod tests { let state = press(state, Button::Next); assert_eq!(state.selection, 6); let state = press(state, Button::Next); + assert_eq!(state.selection, 7); + let state = press(state, Button::Next); assert_eq!(state.selection, 0, "selection wraps after the last row"); } + #[test] + fn settings_change_key_toggles_refresh_quality() { + let mut state = press(ReaderState::boot(), Button::Next); + state.selection = 7; + + let state = press(state, Button::Confirm); + assert_eq!(state.refresh_quality, RefreshQuality::Fast); + + let state = press(state, Button::Confirm); + assert_eq!(state.refresh_quality, RefreshQuality::Normal); + } + #[test] fn settings_change_key_toggles_front_buttons() { let mut state = press(ReaderState::boot(), Button::Next); @@ -5188,6 +5236,7 @@ mod tests { page_count: 0, reading_orientation: DisplayOrientation::PortraitButtonsRight as u8, refresh_policy: RefreshPolicy::FastOnly as u8, + refresh_quality: RefreshQuality::Fast as u8, font_size: FontSize::Large as u8, line_spacing: LineSpacing::Compact as u8, font_weight: FontWeight::Normal as u8, @@ -5487,6 +5536,7 @@ mod tests { shell_orientation: 0, reading_orientation: 0, refresh_policy: 0, + refresh_quality: 0, font_size: 0, line_spacing: 0, font_weight: 0, diff --git a/app-core/src/storage_loop.rs b/app-core/src/storage_loop.rs index 8785e419..9eb94fe2 100644 --- a/app-core/src/storage_loop.rs +++ b/app-core/src/storage_loop.rs @@ -626,6 +626,7 @@ impl OpenSequence { shell_orientation: 0, reading_orientation: 0, refresh_policy: 0, + refresh_quality: 0, font_size: 0, line_spacing: 0, font_weight: 0, @@ -756,6 +757,7 @@ mod tests { shell_orientation: 1, reading_orientation: 2, refresh_policy: 3, + refresh_quality: 0, font_size: 4, line_spacing: 5, font_weight: 6, diff --git a/display/src/epd/ssd1677.rs b/display/src/epd/ssd1677.rs index 860ec0c4..c97f0585 100644 --- a/display/src/epd/ssd1677.rs +++ b/display/src/epd/ssd1677.rs @@ -130,7 +130,12 @@ pub const fn ram_y_counter(rect: Rect) -> [u8; 2] { [bottom as u8, (bottom >> 8) as u8] } -pub const fn update_control_2(mode: RefreshMode, screen_is_on: bool, turn_off: bool) -> u8 { +pub const fn update_control_2( + mode: RefreshMode, + screen_is_on: bool, + turn_off: bool, + fast_du: bool, +) -> u8 { let mut value = 0; if !screen_is_on { value |= 0xC0; @@ -140,7 +145,8 @@ pub const fn update_control_2(mode: RefreshMode, screen_is_on: bool, turn_off: b } match mode { RefreshMode::Full => value | 0x34, - RefreshMode::Fast => value | 0x1C, + RefreshMode::Fast if fast_du => value | 0x1C, + RefreshMode::Fast => value | 0x18, // Load LUT (display mode 1) + display, deliberately without the // 0x20 load-temperature bit so the FAST_CLEAN_TEMPERATURE override // written via 0x1A decides which OTP waveform runs. @@ -159,3 +165,19 @@ pub const fn update_control_1(mode: RefreshMode) -> [u8; 2] { pub const fn is_byte_aligned(rect: Rect) -> bool { rect.x & 7 == 0 && rect.w & 7 == 0 && rect.w > 0 && rect.h > 0 && rect.x < WIDTH as u16 } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_update_control_2_fast_du() { + // Fast with fast_du = true uses 0x1C (the DU shortcut). + assert_eq!(update_control_2(RefreshMode::Fast, true, false, true), 0x1C); + // Fast with fast_du = false uses 0x18 (the stock temperature-compensated sequence). + assert_eq!( + update_control_2(RefreshMode::Fast, true, false, false), + 0x18 + ); + } +} diff --git a/fixtures/golden/landscape-settings-x3.png b/fixtures/golden/landscape-settings-x3.png index 14eab6d0..734f94f5 100644 Binary files a/fixtures/golden/landscape-settings-x3.png and b/fixtures/golden/landscape-settings-x3.png differ diff --git a/fixtures/golden/landscape-settings.png b/fixtures/golden/landscape-settings.png index 385c66f0..32f2ab0c 100644 Binary files a/fixtures/golden/landscape-settings.png and b/fixtures/golden/landscape-settings.png differ diff --git a/fixtures/golden/settings-type-x3.png b/fixtures/golden/settings-type-x3.png index 538d7ae4..f582c78e 100644 Binary files a/fixtures/golden/settings-type-x3.png and b/fixtures/golden/settings-type-x3.png differ diff --git a/fixtures/golden/settings-type.png b/fixtures/golden/settings-type.png index 997835af..411e2d76 100644 Binary files a/fixtures/golden/settings-type.png and b/fixtures/golden/settings-type.png differ diff --git a/fw/src/display_flush/ssd1677.rs b/fw/src/display_flush/ssd1677.rs index fb9a62a2..5d026b52 100644 --- a/fw/src/display_flush/ssd1677.rs +++ b/fw/src/display_flush/ssd1677.rs @@ -30,6 +30,7 @@ pub(crate) async fn flush( screen_on: bool, mode: RefreshMode, prev_staged: bool, + fast_du: bool, ) -> Result<(), PanelError> { let bw_start = Instant::now(); write_ram(epd, CMD_WRITE_RAM_BW, fb).await?; @@ -69,7 +70,7 @@ pub(crate) async fn flush( .await?; epd.command( CMD_DISPLAY_UPDATE_CTRL2, - &[update_control_2(mode, screen_on, false)], + &[update_control_2(mode, screen_on, false, fast_du)], ) .await?; epd.command(CMD_MASTER_ACTIVATION, &[]).await?; @@ -111,7 +112,7 @@ pub(crate) async fn sleep_panel(epd: &mut Epd) -> Result<(), PanelError> { ); epd.command( CMD_DISPLAY_UPDATE_CTRL2, - &[update_control_2(RefreshMode::PowerDown, true, false)], + &[update_control_2(RefreshMode::PowerDown, true, false, false)], ) .await?; epd.command(CMD_MASTER_ACTIVATION, &[]).await?; diff --git a/fw/src/display_flush/uc8253.rs b/fw/src/display_flush/uc8253.rs index 1499cbe3..af9d0cb4 100644 --- a/fw/src/display_flush/uc8253.rs +++ b/fw/src/display_flush/uc8253.rs @@ -67,6 +67,7 @@ pub(crate) async fn flush( _screen_on: bool, mode: RefreshMode, prev_staged: bool, + _fast_du: bool, ) -> Result<(), PanelError> { let plan = flush_plan(mode, SCREEN_POWERED.load(Ordering::Relaxed), prev_staged); bench_log!( diff --git a/fw/src/tasks/display.rs b/fw/src/tasks/display.rs index 47f776da..40ecc618 100644 --- a/fw/src/tasks/display.rs +++ b/fw/src/tasks/display.rs @@ -446,6 +446,7 @@ pub async fn run(mut epd: Epd, mut sd_cs: Output<'static>, deep_sleep_wake: bool refresh_planner.screen_on(), mode, prev_prestaged, + request.refresh_quality == app_core::RefreshQuality::Fast, ) .await .is_ok() @@ -692,6 +693,7 @@ pub async fn run(mut epd: Epd, mut sd_cs: Output<'static>, deep_sleep_wake: bool refresh_planner.screen_on(), RefreshMode::Full, prev_prestaged, + false, ) .await .is_ok() @@ -812,6 +814,7 @@ pub async fn run(mut epd: Epd, mut sd_cs: Output<'static>, deep_sleep_wake: bool refresh_planner.screen_on(), mode, prev_prestaged, + loading_request.refresh_quality == app_core::RefreshQuality::Fast, ) .await .is_ok() @@ -2035,6 +2038,7 @@ fn record_for_persisted(library: &ReaderStore, state: PersistedAppState) -> AppS shell_orientation: state.shell_orientation, reading_orientation: state.reading_orientation, refresh_policy: state.refresh_policy, + refresh_quality: state.refresh_quality, font_size: state.font_size, line_spacing: state.line_spacing, font_weight: state.font_weight, @@ -2135,6 +2139,7 @@ fn restore_saved_state( page_count, reading_orientation: record.reading_orientation, refresh_policy: record.refresh_policy, + refresh_quality: record.refresh_quality, font_size: record.font_size, line_spacing: record.line_spacing, font_weight: record.font_weight, @@ -2190,6 +2195,8 @@ fn sleep_request_from_saved_state( library_menu: app_core::LibraryMenu::None, refresh_policy: refresh_policy_from_u8(record.refresh_policy) .unwrap_or(app_core::RefreshPolicy::FullOnWake), + refresh_quality: app_core::refresh_quality_from_u8(record.refresh_quality) + .unwrap_or(app_core::RefreshQuality::Normal), font_size: display::font::FontSize::from_u8(record.font_size) .unwrap_or(display::font::FontSize::Medium), line_spacing: display::font::LineSpacing::from_u8(record.line_spacing) diff --git a/proto/src/nvm.rs b/proto/src/nvm.rs index 834590bd..22bdd876 100644 --- a/proto/src/nvm.rs +++ b/proto/src/nvm.rs @@ -17,6 +17,7 @@ pub struct AppStateRecord { pub font_weight: u8, pub font_family: u8, pub front_buttons: u8, + pub refresh_quality: u8, pub source_hash: u32, pub source_size: u32, } @@ -50,6 +51,7 @@ impl AppStateRecord { font_weight: Self::DEFAULT_FONT_WEIGHT, font_family: Self::DEFAULT_FONT_FAMILY, front_buttons: 0, + refresh_quality: 0, source_hash: 0, source_size: 0, } @@ -73,10 +75,12 @@ impl AppStateRecord { // reserved tail. The font family later took reserved byte 29 and the // front-button layout byte 30: records written before either carry // zero there, which is the respective default (Literata, pages - // right), so no version bump was needed. Byte 31 stays reserved zero. + // right), so no version bump was needed. Byte 31 stores refresh_quality + // (default 0). out[28] = self.font_weight; out[29] = self.font_family; out[30] = self.front_buttons; + out[31] = self.refresh_quality; let checksum = checksum(&out[..32]); write_u32(&mut out, 32, checksum); out @@ -110,6 +114,7 @@ impl AppStateRecord { font_weight: bytes[28], font_family: bytes[29], front_buttons: bytes[30], + refresh_quality: bytes[31], source_hash: read_u32(bytes, 18), source_size: read_u32(bytes, 22), }) @@ -139,6 +144,7 @@ impl AppStateRecord { font_weight: Self::DEFAULT_FONT_WEIGHT, font_family: Self::DEFAULT_FONT_FAMILY, front_buttons: 0, + refresh_quality: 0, source_hash: read_u32(bytes, 18), source_size: read_u32(bytes, 22), }) @@ -160,6 +166,7 @@ impl AppStateRecord { font_weight: Self::DEFAULT_FONT_WEIGHT, font_family: Self::DEFAULT_FONT_FAMILY, front_buttons: 0, + refresh_quality: 0, source_hash: 0, source_size: 0, }) @@ -344,6 +351,7 @@ mod tests { shell_orientation: 2, reading_orientation: 1, refresh_policy: 2, + refresh_quality: 1, font_size: 2, line_spacing: 0, font_weight: 1, @@ -365,7 +373,7 @@ mod tests { const STATE_GOLDEN: [u8; AppStateRecord::ENCODED_LEN] = [ 0x53, 0x4f, 0x34, 0x58, 0x04, 0x02, 0x01, 0x02, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x29, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x40, 0xe2, 0x01, 0x00, 0x02, 0x00, 0x01, 0x01, - 0x01, 0x00, 0xa7, 0x76, 0x1e, 0x60, + 0x01, 0x01, 0x14, 0x75, 0x1e, 0x5f, ]; #[test] diff --git a/tools/emulator/src/panel.rs b/tools/emulator/src/panel.rs index c32dd2ae..5f125bf3 100644 --- a/tools/emulator/src/panel.rs +++ b/tools/emulator/src/panel.rs @@ -135,7 +135,7 @@ impl PanelModel { )?; self.command( CMD_DISPLAY_UPDATE_CTRL2, - &[display::epd::update_control_2(mode, true, false)], + &[display::epd::update_control_2(mode, true, false, false)], )?; self.command(CMD_MASTER_ACTIVATION, &[])?; if mode == RefreshMode::FastClean { @@ -158,6 +158,7 @@ impl PanelModel { RefreshMode::PowerDown, true, false, + false, )], )?; self.command(CMD_MASTER_ACTIVATION, &[])?; diff --git a/tools/preview/src/main.rs b/tools/preview/src/main.rs index 7cb143a4..4caefb5e 100644 --- a/tools/preview/src/main.rs +++ b/tools/preview/src/main.rs @@ -1820,6 +1820,7 @@ fn write_shell_preview(out: &Path, name: &str, view: UiView, selection: u16) -> orientation: UiOrientation::PortraitButtonsLeft, front_pages_left: false, refresh_policy: UiRefreshPolicy::FullOnWake, + refresh_quality: ui::UiRefreshQuality::Normal, font_size: display::font::FontSize::Medium, line_spacing: display::font::LineSpacing::Normal, font_weight: display::font::FontWeight::Normal, diff --git a/ui/src/app_render.rs b/ui/src/app_render.rs index 82d3aa40..9be4b6c4 100644 --- a/ui/src/app_render.rs +++ b/ui/src/app_render.rs @@ -70,6 +70,7 @@ pub fn render_request(fb: &mut Framebuffer, request: RenderRequest, model: &UiRe orientation: ui_orientation(request.orientation), front_pages_left: request.front_buttons == FrontButtons::PagesLeft, refresh_policy: ui_refresh_policy(request.refresh_policy), + refresh_quality: ui_refresh_quality(request.refresh_quality), font_size: request.font_size, line_spacing: request.line_spacing, font_weight: request.font_weight, @@ -272,6 +273,13 @@ fn ui_refresh_policy(policy: RefreshPolicy) -> UiRefreshPolicy { } } +fn ui_refresh_quality(quality: app_core::RefreshQuality) -> crate::UiRefreshQuality { + match quality { + app_core::RefreshQuality::Normal => crate::UiRefreshQuality::Normal, + app_core::RefreshQuality::Fast => crate::UiRefreshQuality::Fast, + } +} + fn button_label(button: Button) -> &'static str { match button { Button::Power => "POWER", diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 410b051f..cd973cf5 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -37,6 +37,12 @@ pub enum UiRefreshPolicy { FullEveryTen, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UiRefreshQuality { + Normal, + Fast, +} + /// Wireless screen lifecycle, mirrored from app-core so the renderer stays /// decoupled from reducer types the way UiView mirrors AppView. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -98,6 +104,7 @@ pub struct UiShell<'a> { /// of it; the key rail's labels follow the buttons. pub front_pages_left: bool, pub refresh_policy: UiRefreshPolicy, + pub refresh_quality: UiRefreshQuality, pub font_size: display::font::FontSize, pub line_spacing: display::font::LineSpacing, pub font_weight: display::font::FontWeight, @@ -154,6 +161,7 @@ mod tests { orientation: UiOrientation::PortraitButtonsRight, front_pages_left: false, refresh_policy: UiRefreshPolicy::FullEveryTen, + refresh_quality: UiRefreshQuality::Normal, font_size: Default::default(), line_spacing: Default::default(), font_weight: Default::default(), diff --git a/ui/src/render.rs b/ui/src/render.rs index 37ba289b..0f8ad035 100644 --- a/ui/src/render.rs +++ b/ui/src/render.rs @@ -8,8 +8,8 @@ //! only — the device does not tell time. use crate::{ - join_qr, UiLibraryStatus, UiOrientation, UiRefreshPolicy, UiShell, UiSyncStatus, UiTocItem, - UiView, + join_qr, UiLibraryStatus, UiOrientation, UiRefreshPolicy, UiRefreshQuality, UiShell, + UiSyncStatus, UiTocItem, UiView, }; use display::fb::{FbFrame, Framebuffer}; use display::font::{ @@ -698,10 +698,10 @@ fn render_settings(fb: &mut Framebuffer, shell: &UiShell<'_>) { dash_key(fb, layout, 3, "next", false); heading(fb, layout, "Settings"); - // Seven rows must clear the landscape footer line, so the settings + // Eight rows must clear the landscape footer line, so the settings // index runs tighter than the Library's ROW_STEP. - const SETTINGS_ROW_STEP: i16 = 52; - let rows: [(&str, &str); 7] = [ + const SETTINGS_ROW_STEP: i16 = 46; + let rows: [(&str, &str); 8] = [ ( "Typeface", font_family_label(shell.font_family, shell.custom_font_name), @@ -710,6 +710,10 @@ fn render_settings(fb: &mut Framebuffer, shell: &UiShell<'_>) { ("Type weight", font_weight_label(shell.font_weight)), ("Line spacing", line_spacing_label(shell.line_spacing)), ("Refresh", refresh_policy_label(shell.refresh_policy)), + ( + "Refresh quality", + refresh_quality_label(shell.refresh_quality), + ), ("Orientation", orientation_label(shell.orientation)), ("Front buttons", front_buttons_label(shell.front_pages_left)), ]; @@ -1363,6 +1367,13 @@ fn refresh_policy_label(policy: UiRefreshPolicy) -> &'static str { } } +fn refresh_quality_label(quality: UiRefreshQuality) -> &'static str { + match quality { + UiRefreshQuality::Normal => "normal", + UiRefreshQuality::Fast => "fast", + } +} + fn orientation_label(orientation: UiOrientation) -> &'static str { match orientation { UiOrientation::LandscapeButtonsBottom => "buttons down",