A fork of pulp-os for the xteink4 adding custom apps
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

perf: comprehensive speed, memory, and power optimizations Speed: - Swap blit_1bpp_270 loop order (x-outer/y-inner) for sequential strip buffer writes; batch source byte reads (strip.rs) - Batch ASCII advance in wrap_proportional with direct glyph array indexing, bypassing per-byte function call chain (paging.rs) - Skip prescan_image_heights for text-only pages via fast MARKER byte scan before expensive ZIP/SD setup (images.rs) - Remove redundant 4KB replay buffer copy in phase 3 RED RAM write; send strip data directly (ssd1677.rs) - Skip phase 3 sync when a deferred transition is queued, avoiding ~15ms of rendering that will be immediately overdrawn (scheduler.rs) Memory (~35KB static savings): - Heap-alloc EpubToc on demand via Option<Box<EpubToc>> (~13KB) - Heap-alloc prefetch buffer as Vec<u8> instead of [u8; 8192] (~8KB) - Reduce TITLE_CAP 96→64 across DirEntry/BmListEntry/EpubMeta (~4.6KB) - Reduce ZipIndex MAX_ENTRIES 512→256 (~4.5KB) - Free BootConsole after boot via heap Box instead of static (~3KB) - Reduce MAX_PAGES 1024→512 (~2KB) - Shrink title_len/max_lines/qa_count from usize to u8 Power: - Adaptive input polling: 10ms during active input, 50ms when idle after 1 second, reducing ADC wakeups by ~80% during reading

hansmrtn 6f325258 41000d46

+170 -86
+4 -6
kernel/src/drivers/ssd1677.rs
··· 14 14 use embedded_hal::spi::SpiDevice; 15 15 use esp_hal::delay::Delay; 16 16 17 - use super::strip::{STRIP_BUF_SIZE, STRIP_COUNT, StripBuffer}; 17 + use super::strip::{STRIP_COUNT, StripBuffer}; 18 18 19 19 pub const WIDTH: u16 = 800; 20 20 pub const HEIGHT: u16 = 480; ··· 222 222 } 223 223 } 224 224 225 - let data_len = strip.data().len(); 226 - let mut replay = [0xFFu8; STRIP_BUF_SIZE]; 227 - replay[..data_len].copy_from_slice(strip.data()); 228 - 225 + // send the same rendered strip to both RAMs directly; 226 + // no replay copy needed since send_data only reads the buffer 229 227 for &ram_cmd in &[cmd::WRITE_RAM_RED, cmd::WRITE_RAM_BW] { 230 228 self.set_partial_ram_area(px, y, pw, rows); 231 229 self.send_command(ram_cmd); 232 - self.send_data(&replay[..data_len]); 230 + self.send_data(strip.data()); 233 231 } 234 232 235 233 y += rows;
+1 -1
kernel/src/drivers/storage.rs
··· 16 16 17 17 pub const PULP_DIR: &str = "_PULP"; 18 18 pub const TITLES_FILE: &str = "TITLES.BIN"; 19 - pub const TITLE_CAP: usize = 96; 19 + pub const TITLE_CAP: usize = 64; 20 20 21 21 // backward-compatible alias 22 22 pub type StorageError = Error;
+22 -13
kernel/src/drivers/strip.rs
··· 3 3 // widgets draw to logical coords, clipped here 4 4 5 5 use embedded_graphics_core::{ 6 - Pixel, 7 6 draw_target::DrawTarget, 8 7 geometry::{OriginDimensions, Size}, 9 8 pixelcolor::BinaryColor, 10 9 primitives::Rectangle, 10 + Pixel, 11 11 }; 12 12 13 - use super::ssd1677::{HEIGHT, Rotation, WIDTH}; 13 + use super::ssd1677::{Rotation, HEIGHT, WIDTH}; 14 14 use crate::ui::Region; 15 15 16 16 pub const STRIP_ROWS: u16 = 40; ··· 224 224 debug_assert!(gy + y0 as i32 >= wx, "blit_1bpp_270: buf_x underflow"); 225 225 let base_buf_y = base_buf_y_i as usize; 226 226 227 - for y in y0..y1 { 228 - let row = offset + y * stride; 229 - let buf_x = (gy + y as i32 - wx) as usize; 230 - let byte_col = buf_x / 8; 231 - let mask = 1u8 << (7 - (buf_x & 7)); 227 + // loop order: x-outer (strip rows, sequential memory), y-inner 228 + // (strip columns, nearby bytes in same row). this is the opposite 229 + // of the source row-major order but gives much better cache locality 230 + // for the destination strip buffer writes. 231 + for x in x0..x1 { 232 + let src_byte_idx = x / 8; 233 + let src_bit = 1u8 << (7 - (x & 7)); 234 + let dst_row_base = (base_buf_y - x) * rb; 232 235 233 236 if black { 234 - for x in x0..x1 { 235 - if bitmaps[row + x / 8] & (1 << (7 - (x & 7))) != 0 { 236 - self.buf[byte_col + (base_buf_y - x) * rb] &= !mask; 237 + for y in y0..y1 { 238 + if bitmaps[offset + y * stride + src_byte_idx] & src_bit != 0 { 239 + let buf_x = (gy + y as i32 - wx) as usize; 240 + let byte_col = buf_x / 8; 241 + let inv_mask = !(1u8 << (7 - (buf_x & 7))); 242 + self.buf[dst_row_base + byte_col] &= inv_mask; 237 243 } 238 244 } 239 245 } else { 240 - for x in x0..x1 { 241 - if bitmaps[row + x / 8] & (1 << (7 - (x & 7))) != 0 { 242 - self.buf[byte_col + (base_buf_y - x) * rb] |= mask; 246 + for y in y0..y1 { 247 + if bitmaps[offset + y * stride + src_byte_idx] & src_bit != 0 { 248 + let buf_x = (gy + y as i32 - wx) as usize; 249 + let byte_col = buf_x / 8; 250 + let mask = 1u8 << (7 - (buf_x & 7)); 251 + self.buf[dst_row_base + byte_col] |= mask; 243 252 } 244 253 } 245 254 }
+6 -2
kernel/src/kernel/scheduler.rs
··· 316 316 let (deferred, sleep) = self.busy_wait_with_background(app_mgr).await; 317 317 sleep_requested = sleep; 318 318 319 - if app_mgr.has_redraw() { 320 - // content changed mid-DU; leave RED stale 319 + // skip phase 3 when content changed mid-DU or 320 + // a deferred transition is queued (the screen 321 + // will be redrawn immediately after); the next 322 + // partial will use inv_red to compensate for 323 + // the desynchronised RED RAM 324 + if app_mgr.has_redraw() || deferred.is_some() { 321 325 app_mgr.ctx_mut().mark_dirty(r); 322 326 self.red_stale = true; 323 327 self.partial_refreshes += 1;
+11 -2
kernel/src/kernel/tasks.rs
··· 26 26 27 27 #[embassy_executor::task] 28 28 pub async fn input_task(mut input: InputDriver) -> ! { 29 - let mut ticker = Ticker::every(Duration::from_millis(timing::INPUT_TICK_MS)); 30 29 let mut battery_counter: u32 = 0; 30 + let mut idle_ticks: u32 = 0; 31 31 32 32 let raw = input.read_battery_mv(); 33 33 BATTERY_MV.signal(battery::adc_to_battery_mv(raw)); 34 34 35 35 loop { 36 - ticker.next().await; 36 + // adaptive polling: fast rate during active input, slow when idle 37 + let tick_ms = if idle_ticks >= timing::INPUT_IDLE_TICKS { 38 + timing::INPUT_TICK_SLOW_MS 39 + } else { 40 + timing::INPUT_TICK_FAST_MS 41 + }; 42 + Timer::after(Duration::from_millis(tick_ms)).await; 37 43 38 44 if RESET_HOLD.try_take().is_some() { 39 45 input.reset_hold_state(); ··· 42 48 if let Some(ev) = input.poll() { 43 49 let _ = INPUT_EVENTS.try_send(ev); 44 50 IDLE_RESET.signal(()); 51 + idle_ticks = 0; // reset to fast polling on any event 52 + } else { 53 + idle_ticks = idle_ticks.saturating_add(1); 45 54 } 46 55 47 56 battery_counter += 1;
+6 -3
kernel/src/kernel/timing.rs
··· 8 8 // controls how often the event loop wakes to check for work 9 9 pub const TICK_MS: u64 = 10; 10 10 11 - // input task poll interval (ms) 12 - // ADC button sampling rate; should probably(?) match or equal TICK_MS 13 - pub const INPUT_TICK_MS: u64 = 10; 11 + // input task poll intervals (ms) 12 + // fast rate used during active input; slow rate when idle to save power 13 + pub const INPUT_TICK_FAST_MS: u64 = 10; 14 + pub const INPUT_TICK_SLOW_MS: u64 = 50; 15 + // number of ticks at fast rate after the last event before switching to slow 16 + pub const INPUT_IDLE_TICKS: u32 = 100; // 100 * 10ms = 1 second 14 17 15 18 // button debounce window (ms) 16 19 // raw input must be stable for this duration before registering
+3 -3
src/apps/reader/epubs.rs
··· 399 399 if tlen > 0 { 400 400 let n = tlen.min(self.title.len()); 401 401 self.title[..n].copy_from_slice(&self.epub.meta.title[..n]); 402 - self.title_len = n; 402 + self.title_len = n as u8; 403 403 404 404 if let Err(e) = k.save_title(name, self.epub.meta.title_str()) { 405 405 log::warn!("epub: failed to save title mapping: {}", e); 406 406 } 407 407 } 408 408 409 - self.epub.toc.clear(); 409 + self.epub.toc = None; 410 410 411 411 Ok(()) 412 412 } ··· 462 462 if ch >= spine_len { 463 463 let _ = self.epub.finish_cache( 464 464 k, 465 - &self.title[..self.title_len], 465 + &self.title[..self.title_len as usize], 466 466 &self.filename[..self.filename_len], 467 467 ); 468 468 self.epub.img_cache_ch = self.epub.chapter;
+9
src/apps/reader/images.rs
··· 331 331 return; 332 332 } 333 333 334 + // fast path: skip all setup and SD I/O when the page buffer 335 + // contains no image markers (the common case for text pages) 336 + if !self.pg.buf[..buf_len].contains(&MARKER) { 337 + return; 338 + } 339 + 334 340 let ch_zip_idx = self.epub.spine.items[self.epub.chapter as usize] as usize; 335 341 let ch_path = self.epub.zip.entry_name(ch_zip_idx); 336 342 let ch_dir = ch_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(""); ··· 427 433 } 428 434 429 435 self.pg.prefetch_page = NO_PREFETCH; 436 + if self.pg.prefetch.len() < PAGE_BUF { 437 + self.pg.prefetch.resize(PAGE_BUF, 0); 438 + } 430 439 431 440 let dir_buf = self.epub.cache_dir; 432 441 let dir = cache::dir_name_str(&dir_buf);
+36 -31
src/apps/reader/mod.rs
··· 7 7 use crate::apps::PendingSetting; 8 8 use crate::fonts::bitmap::{self, BitmapFont}; 9 9 10 + use alloc::boxed::Box; 10 11 use alloc::vec::Vec; 11 12 use core::fmt::Write; 12 13 ··· 53 54 54 55 pub(super) const PAGE_BUF: usize = 8192; 55 56 56 - pub(super) const MAX_PAGES: usize = 1024; 57 + pub(super) const MAX_PAGES: usize = 512; 57 58 58 59 pub(super) const HEADER_REGION: Region = Region::new(MARGIN, HEADER_Y, HEADER_W, HEADER_H); 59 60 ··· 207 208 pub(super) lines: [LineSpan; LINES_PER_PAGE], 208 209 pub(super) line_count: usize, 209 210 210 - pub(super) prefetch: [u8; PAGE_BUF], 211 + pub(super) prefetch: Vec<u8>, 211 212 pub(super) prefetch_len: usize, 212 213 pub(super) prefetch_page: usize, 213 214 } ··· 223 224 buf_len: 0, 224 225 lines: [LineSpan::EMPTY; LINES_PER_PAGE], 225 226 line_count: 0, 226 - prefetch: [0u8; PAGE_BUF], 227 + prefetch: Vec::new(), 227 228 prefetch_len: 0, 228 229 prefetch_page: NO_PREFETCH, 229 230 } ··· 257 258 pub(super) img_found_count: u16, 258 259 pub(super) img_cached_count: u16, 259 260 260 - pub(super) toc: EpubToc, 261 + pub(super) toc: Option<Box<EpubToc>>, 261 262 pub(super) toc_source: Option<TocSource>, 262 263 pub(super) toc_selected: usize, 263 264 pub(super) toc_scroll: usize, ··· 291 292 skip_large_img: false, 292 293 img_found_count: 0, 293 294 img_cached_count: 0, 294 - toc: EpubToc::new(), 295 + toc: None, 295 296 toc_source: None, 296 297 toc_selected: 0, 297 298 toc_scroll: 0, ··· 327 328 pub struct ReaderApp { 328 329 pub(super) filename: [u8; 32], 329 330 pub(super) filename_len: usize, 330 - pub(super) title: [u8; 96], 331 - pub(super) title_len: usize, 331 + pub(super) title: [u8; 64], 332 + pub(super) title_len: u8, 332 333 pub(super) file_size: u32, 333 334 334 335 pub(super) pg: PageState, ··· 349 350 pub(super) fonts: Option<fonts::FontSet>, 350 351 pub(super) font_line_h: u16, 351 352 pub(super) font_ascent: u16, 352 - pub(super) max_lines: usize, 353 + pub(super) max_lines: u8, 353 354 354 355 // reading theme: runtime layout derived from READING_THEMES 355 356 pub(super) text_margin: u16, // horizontal margin for text content (from theme) ··· 369 370 370 371 pub(super) chrome_font: Option<&'static BitmapFont>, 371 372 pub(super) qa_buf: [QuickAction; QA_MAX], 372 - pub(super) qa_count: usize, 373 + pub(super) qa_count: u8, 373 374 } 374 375 375 376 impl ReaderApp { ··· 377 378 Self { 378 379 filename: [0u8; 32], 379 380 filename_len: 0, 380 - title: [0u8; 96], 381 + title: [0u8; 64], 381 382 title_len: 0, 382 383 file_size: 0, 383 384 ··· 399 400 fonts: None, 400 401 font_line_h: LINE_H, 401 402 font_ascent: LINE_H, 402 - max_lines: LINES_PER_PAGE, 403 + max_lines: LINES_PER_PAGE as u8, 403 404 404 405 text_margin: MARGIN, 405 406 text_y: TEXT_Y, ··· 558 559 n += 1; 559 560 } 560 561 561 - if self.is_epub && !self.epub.toc.is_empty() { 562 + if self.is_epub && self.epub.toc.as_ref().map_or(false, |t| !t.is_empty()) { 562 563 self.qa_buf[n] = QuickAction::trigger(QA_TOC, "Contents", "Open"); 563 564 n += 1; 564 565 } 565 566 566 - self.qa_count = n; 567 + self.qa_count = n as u8; 567 568 } 568 569 569 570 fn apply_font_metrics(&mut self) { 570 571 self.fonts = None; 571 572 self.font_line_h = LINE_H; 572 573 self.font_ascent = LINE_H; 573 - self.max_lines = LINES_PER_PAGE; 574 + self.max_lines = LINES_PER_PAGE as u8; 574 575 575 576 let theme = crate::kernel::config::reading_theme(self.reading_theme_idx); 576 577 let spacing_pct = theme.line_spacing_pct; ··· 581 582 // apply line spacing: scale native line height by theme percentage 582 583 self.font_line_h = ((native_h as u32 * spacing_pct as u32) / 100).max(1) as u16; 583 584 self.font_ascent = fs.ascent(fonts::Style::Regular); 584 - self.max_lines = ((self.text_area_h / self.font_line_h) as usize).min(LINES_PER_PAGE); 585 + self.max_lines = ((self.text_area_h / self.font_line_h) as usize).min(LINES_PER_PAGE) as u8; 585 586 log::info!( 586 587 "font: size_idx={} line_h={} (native {} x {}%) ascent={} max_lines={} margin={}", 587 588 self.book_font_size_idx, ··· 708 709 709 710 fn display_name(&self) -> &str { 710 711 if self.title_len > 0 { 711 - core::str::from_utf8(&self.title[..self.title_len]).unwrap_or(self.name()) 712 + core::str::from_utf8(&self.title[..self.title_len as usize]).unwrap_or(self.name()) 712 713 } else { 713 714 self.name() 714 715 } ··· 824 825 825 826 let n = self.filename_len.min(self.title.len()); 826 827 self.title[..n].copy_from_slice(&self.filename[..n]); 827 - self.title_len = n; 828 + self.title_len = n as u8; 828 829 829 830 // Bump to a new work-queue generation and drain stale work 830 831 // from any previous book (covers the case where on_enter is ··· 876 877 self.page_img = None; 877 878 878 879 if self.is_epub { 879 - self.epub.toc.clear(); 880 + self.epub.toc = None; 880 881 self.epub.toc_source = None; 881 882 } 882 883 } ··· 983 984 984 985 match extract_zip_entry(k, name, &self.epub.zip, toc_idx) { 985 986 Ok(toc_data) => { 987 + let mut toc = Box::new(EpubToc::new()); 986 988 epub::parse_toc( 987 989 source, 988 990 &toc_data, 989 991 toc_dir, 990 992 &self.epub.spine, 991 993 &self.epub.zip, 992 - &mut self.epub.toc, 994 + &mut toc, 993 995 ); 994 - log::info!("epub: TOC has {} entries", self.epub.toc.len()); 996 + log::info!("epub: TOC has {} entries", toc.len()); 997 + self.epub.toc = Some(toc); 995 998 } 996 999 Err(_e) => { 997 1000 log::warn!("epub: failed to read TOC"); ··· 1176 1179 return Transition::None; 1177 1180 } 1178 1181 ActionEvent::Press(Action::Next) | ActionEvent::Repeat(Action::Next) => { 1179 - let len = self.epub.toc.len(); 1182 + let len = self.epub.toc.as_ref().map_or(0, |t| t.len()); 1180 1183 if len > 0 { 1181 1184 if self.epub.toc_selected + 1 < len { 1182 1185 self.epub.toc_selected += 1; ··· 1193 1196 return Transition::None; 1194 1197 } 1195 1198 ActionEvent::Press(Action::Prev) | ActionEvent::Repeat(Action::Prev) => { 1196 - let len = self.epub.toc.len(); 1199 + let len = self.epub.toc.as_ref().map_or(0, |t| t.len()); 1197 1200 if len > 0 { 1198 1201 if self.epub.toc_selected > 0 { 1199 1202 self.epub.toc_selected -= 1; ··· 1212 1215 return Transition::None; 1213 1216 } 1214 1217 ActionEvent::Press(Action::Select) | ActionEvent::Press(Action::NextJump) => { 1215 - let entry = &self.epub.toc.entries[self.epub.toc_selected]; 1218 + let entry = &self.epub.toc.as_ref().unwrap().entries[self.epub.toc_selected]; 1216 1219 if entry.spine_idx != 0xFFFF { 1217 1220 log::info!( 1218 1221 "toc: jumping to \"{}\" -> spine {}", ··· 1322 1325 } 1323 1326 1324 1327 fn quick_actions(&self) -> &[QuickAction] { 1325 - &self.qa_buf[..self.qa_count] 1328 + &self.qa_buf[..self.qa_count as usize] 1326 1329 } 1327 1330 1328 1331 fn on_quick_trigger(&mut self, id: u8, ctx: &mut AppContext) { ··· 1342 1345 } 1343 1346 } 1344 1347 QA_TOC => { 1345 - if self.is_epub && !self.epub.toc.is_empty() { 1346 - log::info!("toc: opening ({} entries)", self.epub.toc.len()); 1348 + if self.is_epub && self.epub.toc.as_ref().map_or(false, |t| !t.is_empty()) { 1349 + let toc = self.epub.toc.as_ref().unwrap(); 1350 + log::info!("toc: opening ({} entries)", toc.len()); 1347 1351 self.epub.toc_selected = 0; 1348 1352 self.epub.toc_scroll = 0; 1349 - for i in 0..self.epub.toc.len() { 1350 - if self.epub.toc.entries[i].spine_idx == self.epub.chapter { 1353 + for i in 0..toc.len() { 1354 + if toc.entries[i].spine_idx == self.epub.chapter { 1351 1355 self.epub.toc_selected = i; 1352 1356 let vis = (self.text_area_h / self.font_line_h) as usize; 1353 1357 if self.epub.toc_selected >= vis { ··· 1494 1498 } 1495 1499 1496 1500 if self.state == State::ShowToc { 1497 - let toc_len = self.epub.toc.len(); 1501 + let toc_ref = self.epub.toc.as_ref().unwrap(); 1502 + let toc_len = toc_ref.len(); 1498 1503 let tx = self.text_margin as i32; 1499 1504 let ty = self.text_y as i32; 1500 1505 if self.fonts.is_some() { ··· 1505 1510 let visible = vis_max.min(toc_len.saturating_sub(self.epub.toc_scroll)); 1506 1511 for i in 0..visible { 1507 1512 let idx = self.epub.toc_scroll + i; 1508 - let entry = &self.epub.toc.entries[idx]; 1513 + let entry = &toc_ref.entries[idx]; 1509 1514 let y_top = ty + i as i32 * line_h; 1510 1515 let baseline = y_top + ascent; 1511 1516 let selected = idx == self.epub.toc_selected; ··· 1538 1543 let visible = vis_max.min(toc_len.saturating_sub(self.epub.toc_scroll)); 1539 1544 for i in 0..visible { 1540 1545 let idx = self.epub.toc_scroll + i; 1541 - let entry = &self.epub.toc.entries[idx]; 1546 + let entry = &toc_ref.entries[idx]; 1542 1547 let y = ty + i as i32 * LINE_H as i32 + LINE_H as i32; 1543 1548 let marker = if idx == self.epub.toc_selected { 1544 1549 "> "
+64 -21
src/apps/reader/paging.rs
··· 6 6 }; 7 7 8 8 use crate::fonts; 9 + use crate::fonts::bitmap::FIRST_CHAR; 9 10 use crate::kernel::KernelHandle; 10 11 11 12 use super::{ ··· 24 25 n, 25 26 &fs, 26 27 &mut self.pg.lines, 27 - self.max_lines, 28 + self.max_lines as usize, 28 29 self.text_w, 29 30 heights, 30 31 ); ··· 38 39 pub(super) fn wrap_monospace(&mut self, n: usize) -> usize { 39 40 use super::CHARS_PER_LINE; 40 41 41 - let max = self.max_lines; 42 + let max = self.max_lines as usize; 42 43 self.pg.line_count = 0; 43 44 let mut col: usize = 0; 44 45 let mut line_start: usize = 0; ··· 127 128 let name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 128 129 129 130 if self.pg.prefetch_page == self.pg.page { 130 - core::mem::swap(&mut self.pg.buf, &mut self.pg.prefetch); 131 - self.pg.buf_len = self.pg.prefetch_len; 131 + let pf_len = self.pg.prefetch_len; 132 + self.pg.buf[..pf_len].copy_from_slice(&self.pg.prefetch[..pf_len]); 133 + self.pg.buf_len = pf_len; 132 134 self.pg.prefetch_page = NO_PREFETCH; 133 135 self.pg.prefetch_len = 0; 134 136 } else if self.is_epub && self.epub.chapters_cached { ··· 162 164 let next_offset = self.pg.offsets[self.pg.page] + consumed as u32; 163 165 164 166 if self.pg.page + 1 >= self.pg.total_pages && !self.pg.fully_indexed { 165 - if self.pg.line_count >= self.max_lines && next_offset < self.file_size { 167 + if self.pg.line_count >= self.max_lines as usize && next_offset < self.file_size { 166 168 if self.pg.total_pages < MAX_PAGES { 167 169 self.pg.offsets[self.pg.total_pages] = next_offset; 168 170 self.pg.total_pages += 1; ··· 175 177 } 176 178 177 179 if self.pg.page + 1 < self.pg.total_pages { 180 + if self.pg.prefetch.len() < PAGE_BUF { 181 + self.pg.prefetch.resize(PAGE_BUF, 0); 182 + } 178 183 let pf_offset = self.pg.offsets[self.pg.page + 1]; 179 184 let pf_result = if self.is_epub && self.epub.chapters_cached { 180 185 let cf_str = self.epub.cache_file_str(); ··· 222 227 let consumed = self.wrap_lines_counted(n); 223 228 let next_offset = offset + consumed; 224 229 225 - if self.pg.line_count >= self.max_lines && next_offset < total { 230 + if self.pg.line_count >= self.max_lines as usize && next_offset < total { 226 231 self.pg.offsets[self.pg.total_pages] = next_offset as u32; 227 232 self.pg.total_pages += 1; 228 233 offset = next_offset; ··· 583 588 continue; 584 589 } 585 590 591 + // --- ASCII fast path: batch space and word runs --- 586 592 let sty = current_style(bold, italic, heading); 587 - let adv = fonts.advance_byte(b, sty) as u32; 593 + let font = fonts.font(sty); 594 + let glyphs = font.glyphs; 588 595 589 596 if b == b' ' { 597 + let adv = glyphs[(b' ' - FIRST_CHAR) as usize].advance as u32; 590 598 cursor_x += adv; 591 599 last_space = i + 1; 592 600 cursor_at_space = cursor_x; ··· 604 612 continue; 605 613 } 606 614 607 - cursor_x += adv; 608 - if cursor_x > max_w { 609 - if last_space > line_start { 610 - emit!(line_start, last_space); 611 - cursor_x -= cursor_at_space; 612 - line_start = last_space; 613 - } else { 614 - emit!(line_start, i); 615 - line_start = i; 616 - cursor_x = adv; 615 + // Printable non-space ASCII (0x21..=0x7E): batch-scan the word run. 616 + // Find end of contiguous printable non-space ASCII bytes, sum advances. 617 + let word_start = i; 618 + let remaining = max_w.saturating_sub(cursor_x); 619 + let mut run_adv: u32 = 0; 620 + let mut j = i; 621 + while j < n { 622 + let c = buf[j]; 623 + // stop at space, control chars, MARKER, high-bit bytes 624 + if c <= b' ' || c > 0x7E { 625 + break; 617 626 } 618 - last_space = line_start; 619 - cursor_at_space = 0; 620 - if line_count >= max_l { 621 - return (line_start, line_count); 627 + let a = glyphs[(c - FIRST_CHAR) as usize].advance as u32; 628 + if run_adv + a > remaining && j > word_start { 629 + // would overflow; stop batch here so we handle break properly 630 + break; 622 631 } 632 + run_adv += a; 633 + j += 1; 623 634 } 624 635 636 + if j > i { 637 + // consumed j - i bytes as a batch 638 + cursor_x += run_adv; 639 + i = j; 640 + if cursor_x > max_w { 641 + // overflow: break at last space or at word start 642 + if last_space > line_start { 643 + emit!(line_start, last_space); 644 + cursor_x -= cursor_at_space; 645 + line_start = last_space; 646 + } else { 647 + emit!(line_start, word_start); 648 + line_start = word_start; 649 + // recompute cursor_x from line_start..i 650 + cursor_x = 0; 651 + for k in line_start..i { 652 + let c = buf[k]; 653 + if c >= FIRST_CHAR && c <= 0x7E { 654 + cursor_x += glyphs[(c - FIRST_CHAR) as usize].advance as u32; 655 + } 656 + } 657 + } 658 + last_space = line_start; 659 + cursor_at_space = 0; 660 + if line_count >= max_l { 661 + return (line_start, line_count); 662 + } 663 + } 664 + continue; 665 + } 666 + 667 + // single non-printable byte that wasn't caught above; skip 625 668 i += 1; 626 669 } 627 670
+7 -3
src/bin/main.rs
··· 3 3 #![no_std] 4 4 #![no_main] 5 5 6 + extern crate alloc; 7 + 6 8 use esp_backtrace as _; 7 9 use esp_hal::clock::CpuClock; 8 10 use esp_hal::delay::Delay; ··· 45 47 static BUMPS: ConstStaticCell<ButtonFeedback> = ConstStaticCell::new(ButtonFeedback::new()); 46 48 static DIR_CACHE: ConstStaticCell<DirCache> = ConstStaticCell::new(DirCache::new()); 47 49 static BM_CACHE: ConstStaticCell<BookmarkCache> = ConstStaticCell::new(BookmarkCache::new()); 48 - static CONSOLE: ConstStaticCell<BootConsole> = ConstStaticCell::new(BootConsole::new()); 50 + // BootConsole is heap-allocated during boot and dropped after display, 51 + // reclaiming ~3 KB that would otherwise sit unused in .bss forever. 49 52 50 53 static HOME: StaticCell<HomeApp> = StaticCell::new(); 51 54 static FILES: StaticCell<FilesApp> = StaticCell::new(); ··· 62 65 // reclaim ~64 KB from 2nd-stage bootloader; net heap ~172 KB 63 66 esp_alloc::heap_allocator!(#[ram(reclaimed)] size: 64_000); 64 67 65 - let console = CONSOLE.take(); 68 + let mut console = alloc::boxed::Box::new(BootConsole::new()); 66 69 console.push("pulp-os 0.1.0"); 67 70 console.push("esp32c3 rv32imc 160mhz"); 68 71 console.push("heap: 172K (108K + 64K reclaimed)"); ··· 140 143 ); 141 144 142 145 console.push("kernel: constructed"); 143 - kernel.show_boot_console(console).await; 146 + kernel.show_boot_console(&console).await; 147 + drop(console); // reclaim ~3 KB of heap 144 148 145 149 kernel.boot(&mut app_mgr).await; 146 150
+1 -1
src/fonts/mod.rs
··· 161 161 } 162 162 163 163 #[inline] 164 - fn font(&self, style: Style) -> &'static BitmapFont { 164 + pub fn font(&self, style: Style) -> &'static BitmapFont { 165 165 match style { 166 166 Style::Regular => self.regular, 167 167 Style::Bold => self.bold,