move paging to app, split cache from long screen layout

This commit is contained in:
Lukas Bielesch
2026-02-03 16:15:52 +01:00
parent 6bbbc57c12
commit 74ff8cf8fd
8 changed files with 199 additions and 206 deletions

View File

@@ -1,18 +1,4 @@
use crate::{
debug,
ipc::{IpcMessage, RemoteSysTask},
micropython::buffer::StrBuffer,
strutil::TString,
ui::component::{base::AttachType, Event, EventCtx},
};
use core::mem::MaybeUninit;
use rkyv::{
api::low::to_bytes_in_with_alloc,
rancor::Failure,
ser::{allocator::SubAllocator, writer::Buffer},
util::Align,
};
use trezor_structs::UtilEnum;
use crate::{micropython::buffer::StrBuffer, strutil::TString};
/// A node in the linked list cache
struct CacheNode<const T: usize> {
@@ -79,77 +65,43 @@ impl<const T: usize> CacheNode<T> {
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CacheState {
Uninit,
Waiting(usize),
Ready,
}
/// LRU-style linked list cache with N slots.
///
/// head <-> ... <-> tail
/// - Adding next: add after head, evict tail if full
/// - Adding prev: add before tail, evict head if full
pub struct Cache<const A: usize, const T: usize, const N: usize> {
state: CacheState,
pub struct Cache<const T: usize, const N: usize> {
nodes: [CacheNode<T>; N],
head: Option<usize>, // newest towards next direction
tail: Option<usize>, // newest towards prev direction
current_node: usize,
current_page: u16,
total_pages: u16,
current: Option<usize>,
len: usize,
}
impl<const A: usize, const T: usize, const N: usize> Cache<A, T, N> {
impl<const T: usize, const N: usize> Cache<T, N> {
const EMPTY_NODE: CacheNode<T> = CacheNode::empty();
pub const fn new(total_pages: u16) -> Self {
pub const fn new() -> Self {
Self {
state: CacheState::Uninit,
nodes: [Self::EMPTY_NODE; N],
head: None,
tail: None,
current_node: 0,
total_pages,
current_page: 0,
current: None,
len: 0,
}
}
pub fn event(&mut self, ctx: &mut EventCtx, event: Event) {
if matches!(event, Event::Attach(AttachType::Initial)) {
// debug_assert!(self.cache.state == CacheState::Empty);
// Load content into cache if needed
self.request_page(ctx, 0);
}
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if let Some(data) = IpcMessage::try_receive(RemoteSysTask::Unknown(2)) {
self.update(&data.data(), ctx);
} else {
ctx.request_anim_frame();
}
}
pub fn initialized(&self) -> bool {
self.current.is_some()
}
pub fn init(&mut self, data: &[u8]) {
debug_assert!(self.state == CacheState::Uninit);
debug_assert!(self.len == 0);
self.current_node = 0;
self.nodes[self.current_node].set_content(data);
self.insert_at_head(self.current_node);
let current = 0;
self.nodes[current].set_content(data);
self.insert_at_head(current);
self.len = 1;
self.state = CacheState::Ready;
}
pub fn has_next(&self) -> bool {
self.current_page + 1 < self.total_pages
}
pub fn has_prev(&self) -> bool {
self.current_page > 0
self.current = Some(current);
}
/// Find a free node slot
@@ -229,6 +181,8 @@ impl<const A: usize, const T: usize, const N: usize> Cache<A, T, N> {
/// Get a slot for a new node, evicting from specified end if needed
fn get_slot_evict_tail(&mut self) -> usize {
debug_assert!(self.len >= 1);
if let Some(idx) = self.find_free_slot() {
idx
} else {
@@ -237,6 +191,8 @@ impl<const A: usize, const T: usize, const N: usize> Cache<A, T, N> {
}
fn get_slot_evict_head(&mut self) -> usize {
debug_assert!(self.len >= 1);
if let Some(idx) = self.find_free_slot() {
idx
} else {
@@ -244,130 +200,67 @@ impl<const A: usize, const T: usize, const N: usize> Cache<A, T, N> {
}
}
fn request_page(&mut self, ctx: &mut EventCtx, page_idx: u16) {
let data = UtilEnum::RequestSlice {
offset: (page_idx as u32) * (A as u32),
size: A as u32,
};
let mut arena = [MaybeUninit::<u8>::uninit(); 200];
let mut out = Align([MaybeUninit::<u8>::uninit(); 200]);
let bytes = to_bytes_in_with_alloc::<_, _, Failure>(
&data,
Buffer::from(&mut *out),
SubAllocator::new(&mut arena),
)
.unwrap();
let msg = IpcMessage::new(9, &bytes);
unwrap!(msg.send(RemoteSysTask::Unknown(2), 6));
ctx.request_anim_frame();
}
pub fn update(&mut self, data: &[u8], ctx: &mut EventCtx) -> u16 {
debug_assert!(matches!(
self.state,
CacheState::Uninit | CacheState::Waiting(_)
));
match self.state {
CacheState::Uninit => {
self.init(data);
if self.has_next() {
let idx = self.current_page as usize + 1;
self.state = CacheState::Waiting(idx);
self.request_page(ctx, idx as u16);
} else {
self.state = CacheState::Ready;
}
ctx.request_paint();
}
CacheState::Waiting(next_page) if self.current_page + 1 == next_page as u16 => {
debug_assert!(self.head.is_some());
debug_assert!(self.current_node == self.head.unwrap());
self.set_next(data);
}
CacheState::Waiting(prev_page) if self.current_page == prev_page as u16 + 1 => {
debug_assert!(self.tail.is_some());
debug_assert!(self.current_node == self.tail.unwrap());
self.set_prev(data);
}
_ => {
unimplemented!("Unexpected page received");
}
}
0
}
/// Add next page data (adds at head, evicts from tail if full)
pub fn set_next(&mut self, data: &[u8]) {
debug_assert!(self.has_next() && matches!(self.state, CacheState::Waiting(_)));
debug_assert!(self.current_node == self.head.unwrap());
pub fn push_head(&mut self, data: &[u8]) {
debug_assert!(self.current == self.head);
let idx = self.get_slot_evict_tail();
self.nodes[idx].set_content(data);
self.insert_at_head(idx);
self.len += 1;
self.state = CacheState::Ready;
}
/// Add prev page data (adds at tail, evicts from head if full)
pub fn set_prev(&mut self, data: &[u8]) {
debug_assert!(self.has_prev() && matches!(self.state, CacheState::Waiting(_)));
debug_assert!(self.current_node == self.tail.unwrap());
pub fn push_tail(&mut self, data: &[u8]) {
// debug_assert!(self.has_prev() && matches!(self.state,
// CacheState::Waiting(_)));
debug_assert!(self.current == self.tail);
let idx = self.get_slot_evict_head();
self.nodes[idx].set_content(data);
self.insert_at_tail(idx);
self.len += 1;
self.state = CacheState::Ready;
}
/// Switch to next page in cache
pub fn switch_next(&mut self, ctx: &mut EventCtx) {
debug_assert!(self.has_next() && self.state == CacheState::Ready);
pub fn go_next(&mut self) {
debug_assert!(self.current.is_some());
debug_assert!(self.nodes[unwrap!(self.current)].has_next());
let next_node = self.nodes[self.current_node].next;
debug_assert!(next_node.is_some());
self.current_node = next_node.unwrap();
self.current_page += 1;
// Request prefetch if there's another page after
if self.has_next() && self.nodes[self.current_node].next.is_none() {
let next_idx = self.current_page as usize + 1;
self.state = CacheState::Waiting(next_idx);
self.request_page(ctx, next_idx as u16);
}
self.current = self.nodes[unwrap!(self.current)].next;
}
/// Switch to prev page in cache
pub fn switch_prev(&mut self, ctx: &mut EventCtx) {
debug_assert!(self.has_prev() && self.state == CacheState::Ready);
pub fn go_prev(&mut self) {
debug_assert!(self.current.is_some());
debug_assert!(self.nodes[unwrap!(self.current)].has_prev());
let prev_node = self.nodes[self.current_node].prev;
debug_assert!(prev_node.is_some());
self.current_node = prev_node.unwrap();
self.current_page -= 1;
// Request prefetch if there's another page before
if self.has_prev() && self.nodes[self.current_node].prev.is_none() {
let prev_idx = self.current_page as usize - 1;
self.state = CacheState::Waiting(prev_idx);
self.request_page(ctx, prev_idx as u16);
}
self.current = self.nodes[unwrap!(self.current)].prev;
}
pub fn current_page_data(&self) -> TString<'static> {
if self.state == CacheState::Uninit {
return TString::empty();
pub fn is_at_head(&self) -> bool {
debug_assert!(self.current.is_some());
self.current == self.head
}
pub fn is_at_tail(&self) -> bool {
debug_assert!(self.current.is_some());
self.current == self.tail
}
pub fn current_data(&self) -> Option<TString<'static>> {
if let Some(current) = self.current {
unsafe {
return Some(
StrBuffer::from_ptr_and_len(
self.nodes[current].content.as_ptr(),
self.nodes[current].content_len,
)
.into(),
);
}
} else {
None
}
let node = &self.nodes[self.current_node];
debug_assert!(node.valid);
unsafe { StrBuffer::from_ptr_and_len(node.content.as_ptr(), node.content_len) }.into()
}
}
/// Type alias for the default cache configuration
pub type PageCache<const A: usize, const T: usize> = Cache<A, T, 3>;
pub type PageCache = Cache<360, 3>;

View File

@@ -1,8 +1,10 @@
use crate::{
ipc::{IpcMessage, RemoteSysTask},
strutil::TString,
ui::{
cache::PageCache,
component::{
base::AttachType,
text::{layout::LayoutFit, LineBreaking},
Component, Event, EventCtx, Never, TextLayout,
},
@@ -12,9 +14,16 @@ use crate::{
},
};
use super::{constant::SCREEN, theme, ActionBar, ActionBarMsg, Header, Hint};
use core::mem::MaybeUninit;
use rkyv::{
api::low::to_bytes_in_with_alloc,
rancor::Failure,
ser::{allocator::SubAllocator, writer::Buffer},
util::Align,
};
use trezor_structs::UtilEnum;
pub const CHARS_PER_PAGE: usize = 10;
use super::{constant::SCREEN, theme, ActionBar, ActionBarMsg, Header, Hint};
pub enum LongContentScreenMsg {
Confirmed,
@@ -29,8 +38,8 @@ pub struct LongContentScreen<'a> {
}
impl<'a> LongContentScreen<'a> {
pub fn new(title: TString<'static>, text_length: u32) -> Self {
let content = LongContent::new(text_length);
pub fn new(title: TString<'static>, pages: usize) -> Self {
let content = LongContent::new(pages as u16);
let mut action_bar = ActionBar::new_cancel_confirm();
action_bar.update(content.pager);
@@ -127,20 +136,18 @@ impl<'a> crate::trace::Trace for LongContentScreen<'a> {
struct LongContent {
pager: Pager,
content_length: u32,
cache: PageCache<CHARS_PER_PAGE, { 4 * CHARS_PER_PAGE }>,
cache: PageCache,
area: Rect,
state: ContentState,
}
impl LongContent {
fn new(content_length: u32) -> Self {
let total_pages =
((content_length as u16) + CHARS_PER_PAGE as u16 - 1) / CHARS_PER_PAGE as u16;
fn new(pages: u16) -> Self {
Self {
pager: Pager::new(total_pages),
content_length,
cache: PageCache::<CHARS_PER_PAGE, { 4 * CHARS_PER_PAGE }>::new(total_pages),
pager: Pager::new(pages),
cache: PageCache::new(),
area: Rect::zero(),
state: ContentState::Uninit,
}
}
@@ -150,14 +157,48 @@ impl LongContent {
fn switch_next(&mut self, ctx: &mut EventCtx) {
debug_assert!(!self.pager.is_last());
debug_assert!(matches!(self.state, ContentState::Ready));
self.pager.goto_next();
self.cache.switch_next(ctx);
self.cache.go_next();
// Request prefetch if there's another page after
if self.pager.has_next() && self.cache.is_at_head() {
let next = self.pager.next() as usize;
self.request_page(ctx, next);
self.state = ContentState::Waiting(next);
}
}
fn switch_prev(&mut self, ctx: &mut EventCtx) {
debug_assert!(!self.pager.is_first());
debug_assert!(matches!(self.state, ContentState::Ready));
self.pager.goto_prev();
self.cache.switch_prev(ctx);
self.cache.go_prev();
// Request prefetch if there's another page before
if self.pager.has_prev() && self.cache.is_at_tail() {
let prev = self.pager.prev() as usize;
self.request_page(ctx, prev);
self.state = ContentState::Waiting(prev);
}
}
fn request_page(&mut self, ctx: &mut EventCtx, idx: usize) {
let data = UtilEnum::RequestPage { idx };
let mut arena = [MaybeUninit::<u8>::uninit(); 200];
let mut out = Align([MaybeUninit::<u8>::uninit(); 200]);
let bytes = to_bytes_in_with_alloc::<_, _, Failure>(
&data,
Buffer::from(&mut *out),
SubAllocator::new(&mut arena),
)
.unwrap();
let msg = IpcMessage::new(9, &bytes);
unwrap!(msg.send(RemoteSysTask::Unknown(2), 6));
ctx.request_anim_frame();
}
}
@@ -170,13 +211,58 @@ impl Component for LongContent {
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.cache.event(ctx, event);
if matches!(event, Event::Attach(AttachType::Initial)) {
// debug_assert!(self.cache.state == CacheState::Empty);
// Load content into cache if needed
self.request_page(ctx, 0);
}
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if let Some(data) = IpcMessage::try_receive(RemoteSysTask::Unknown(2)) {
debug_assert!(matches!(
self.state,
ContentState::Uninit | ContentState::Waiting(_)
));
self.state = match self.state {
ContentState::Uninit => {
self.cache.init(data.data());
ctx.request_paint();
if self.pager.has_next() {
let idx = self.pager.next() as usize;
self.request_page(ctx, idx);
ContentState::Waiting(idx)
} else {
ContentState::Ready
}
}
ContentState::Waiting(next_page)
if self.pager.current() + 1 == next_page as u16 =>
{
debug_assert!(self.cache.is_at_head());
self.cache.push_head(data.data());
ContentState::Ready
}
ContentState::Waiting(prev_page)
if self.pager.current() == prev_page as u16 + 1 =>
{
debug_assert!(self.cache.is_at_tail());
self.cache.push_tail(data.data());
ContentState::Ready
}
_ => {
unimplemented!("Unexpected page received");
}
}
} else {
ctx.request_anim_frame();
}
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let current_page = self.cache.current_page_data();
let current_page = self.cache.current_data().unwrap_or(TString::empty());
current_page.map(|text| {
let layout = TextLayout::new(
@@ -196,8 +282,14 @@ impl Component for LongContent {
impl crate::trace::Trace for LongContent {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("LongContent");
t.int("content length", self.content_length as i64);
t.int("current_page", self.pager.current() as i64);
t.int("total_pages", self.pager.total() as i64);
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ContentState {
Uninit,
Waiting(usize),
Ready,
}

View File

@@ -124,11 +124,8 @@ impl FirmwareUI for UIEckhart {
Ok(layout)
}
fn confirm_long(
title: TString<'static>,
content_length: u32,
) -> Result<impl LayoutMaybeTrace, Error> {
let screen = LongContentScreen::new(title, content_length);
fn confirm_long(title: TString<'static>, pages: usize) -> Result<impl LayoutMaybeTrace, Error> {
let screen = LongContentScreen::new(title, pages);
let layout = RootComponent::new(screen);
Ok(layout)
}
@@ -1776,10 +1773,12 @@ impl FirmwareUI for UIEckhart {
)?;
LayoutObj::new_root(layout)
}
ArchivedTrezorUiEnum::ConfirmLong { title, content_len } => {
ArchivedTrezorUiEnum::ConfirmLong { title, pages } => {
// Use safe Deref trait instead of raw pointers
let layout =
Self::confirm_long(tstr_from_archived(title), u32::from(*content_len))?;
let layout = Self::confirm_long(
tstr_from_archived(title),
unwrap!(usize::try_from(pages.to_native())),
)?;
LayoutObj::new_root(layout)
}
ArchivedTrezorUiEnum::ConfirmProperties { title, props } => {

View File

@@ -41,7 +41,7 @@ pub trait FirmwareUI {
fn confirm_long(
title: TString<'static>,
content_length: u32,
pages: usize,
) -> Result<impl LayoutMaybeTrace, Error>;
fn confirm_address(

View File

@@ -3,8 +3,17 @@ use alloc::string::ToString;
use trezor_app_sdk::{Result, crypto, log, ui};
pub fn get_public_key(msg: EthereumGetPublicKey) -> Result<EthereumPublicKey> {
let long_string: &str = "Hello, 世界! 🌍🌎🌏 Привет мир! مرحبا بالعالم 日本語テスト αβγδε ñoño café naïve 北京 Zürich™ €100 ½ ¼ ¾ → ← ↑ ↓ ♠♣♥♦ ✓✗ ∑∏∫∂ ≤≥≠≈ 🎉🔥💡🚀 ⚡️☀️🌙⭐️";
log::info!("string chars: {}, string bytes: {}", long_string.chars().count(), long_string.len());
let long_string: &str = "asdfghjklqwertyuiopzxcvbnmASDFGHJKLQWERTYUIOPZXCVBNM0123456789\
asdfghjklqwertyuiopzxcvbnmASDFGHJKLQWERTYUIOPZXCVBNM0123456789\
asdfghjklqwertyuiopzxcvbnmASDFGHJKLQWERTYUIOPZXCVBNM0123456789\
asdfghjklqwertyuiopzxcvbnmASDFGHJKLQWERTYUIOPZXCVBNM0123456789\
asdfghjklqwertyuiopzxcvbnmASDFGHJKLQWERTYUIOPZXCVBNM0123456789\
asdfghjklqwertyuiopzxcvbnmASDFGHJKLQWERTYUIOPZXCVBNM0123456789";
log::info!(
"string chars: {}, string bytes: {}",
long_string.chars().count(),
long_string.len()
);
ui::confirm_long_value("title", long_string)?;

View File

@@ -96,20 +96,23 @@ impl<'a, T: Into<u16>> IpcRemote<'a, T> {
let archived = unsafe { rkyv::access_unchecked::<ArchivedUtilEnum>(data) };
match archived {
ArchivedUtilEnum::RequestSlice { offset, size } => {
let offset_val = offset.to_native() as usize;
let size_val = size.to_native() as usize;
ArchivedUtilEnum::RequestPage { idx } => {
let page_idx = idx.to_native() as usize;
// Find byte range for the requested char slice
let mut chars = long_content.chars();
let start_byte = chars
.by_ref()
.take(offset_val)
.take(page_idx * crate::ui::CHARS_PER_PAGE)
.map(|c| c.len_utf8())
.sum::<usize>();
let slice_len = chars
.take(crate::ui::CHARS_PER_PAGE)
.map(|c| c.len_utf8())
.sum::<usize>();
let slice_len = chars.take(size_val).map(|c| c.len_utf8()).sum::<usize>();
let slice = &long_content.as_bytes()[start_byte..start_byte + slice_len];
// TODO implement Debuf for Api error
// TODO implement Debug for Api error
let _ = IpcMessage::new(10, slice).send(RemoteSysTask::CoreApp, 6);
}
_ => return Err(Error::UnexpectedResponse(reply)),

View File

@@ -33,6 +33,8 @@ use crate::util::Timeout;
pub type ArchivedTrezorUiResult = Archived<TrezorUiResult>;
pub type ArchivedTrezorUiEnum = Archived<TrezorUiEnum>;
pub const CHARS_PER_PAGE: usize = 96;
// ============================================================================
// Helper Functions
// ============================================================================
@@ -52,11 +54,11 @@ fn ipc_ui_call(value: &TrezorUiEnum) -> UiResult {
Ok(deserialized)
}
fn ipc_ui_long_call(value: &TrezorUiEnum, long_content: &str) -> UiResult {
let bytes = to_bytes::<Failure>(value).unwrap();
let message = IpcMessage::new(0, &bytes);
let result = services_or_die().call_long(CoreIpcService::Ui, &message, Timeout::max(), long_content)?;
let result =
services_or_die().call_long(CoreIpcService::Ui, &message, Timeout::max(), long_content)?;
// Safe validation using bytecheck before accessing archived data
let archived = rkyv::access::<ArchivedTrezorUiResult, Failure>(result.data()).unwrap();
@@ -64,8 +66,6 @@ fn ipc_ui_long_call(value: &TrezorUiEnum, long_content: &str) -> UiResult {
Ok(deserialized)
}
/// Send a UI call and expect a boolean confirmation result
fn ipc_ui_call_confirm(value: TrezorUiEnum) -> UiResult {
match ipc_ui_call(&value) {
@@ -102,7 +102,7 @@ pub fn confirm_value(title: &str, content: &str) -> UiResult {
pub fn confirm_long_value(title: &str, content: &str) -> UiResult {
let value = TrezorUiEnum::ConfirmLong {
title: ShortString::from_str(title).unwrap(),
content_len: content.chars().count() as u32,
pages: (content.chars().count() as usize + CHARS_PER_PAGE - 1) / CHARS_PER_PAGE,
};
match ipc_ui_long_call(&value, content) {

View File

@@ -124,7 +124,7 @@ pub enum TrezorUiEnum {
},
ConfirmLong {
title: ShortString,
content_len: u32,
pages: usize,
},
Warning {
title: ShortString,
@@ -189,8 +189,5 @@ pub enum TrezorCryptoResult {
/// Outgoing Crypto result message for IPC
#[derive(Archive, Serialize, Deserialize)]
pub enum UtilEnum {
RequestSlice {
offset: u32,
size: u32,
},
RequestPage { idx: usize },
}