Off-load some slint allocations to PSRAM

This commit is contained in:
Jakub Hlusička 2026-01-28 22:46:14 +01:00
parent 8426852d7c
commit 3b24825677
9 changed files with 181 additions and 123 deletions

51
firmware/Cargo.lock generated
View file

@ -30,6 +30,7 @@ dependencies = [
"esp-backtrace",
"esp-bootloader-esp-idf",
"esp-hal",
"esp-metadata-generated",
"esp-println",
"esp-radio",
"esp-rtos",
@ -37,7 +38,8 @@ dependencies = [
"esp-sync",
"gix",
"hmac",
"i-slint-common",
"i-slint-common 1.14.1 (registry+https://github.com/rust-lang/crates.io-index)",
"i-slint-core",
"indoc",
"itertools 0.14.0",
"json",
@ -59,6 +61,7 @@ dependencies = [
"slint-build",
"spectre-api-sys",
"static_cell",
"tinyvec",
"xkbcommon 0.9.0 (git+https://github.com/Limeth/xkbcommon-rs?branch=esp32s3)",
"xz2",
]
@ -954,8 +957,6 @@ checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa"
[[package]]
name = "const-field-offset"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91fcde4ca1211b5a94b573083c472ee19e86b19a441913f66e1cc5c41daf0255"
dependencies = [
"const-field-offset-macro",
"field-offset",
@ -964,8 +965,6 @@ dependencies = [
[[package]]
name = "const-field-offset-macro"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5387f5bbc9e9e6c96436ea125afa12614cebf8ac67f49abc08c1e7a891466c90"
dependencies = [
"proc-macro2",
"quote",
@ -3823,13 +3822,11 @@ dependencies = [
[[package]]
name = "i-slint-backend-linuxkms"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8fd06c00fbdac3dd490cf5c10da7daad3820d775060a19ea277d8ab944a160b"
dependencies = [
"bytemuck",
"calloop 0.14.3",
"drm",
"i-slint-common",
"i-slint-common 1.14.1",
"i-slint-core",
"input",
"memmap2",
@ -3840,13 +3837,11 @@ dependencies = [
[[package]]
name = "i-slint-backend-selector"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e138660c634d6bbdf98bb2d0cfa487cda28e032e133a2a2c974f1cc494198765"
dependencies = [
"cfg-if",
"i-slint-backend-linuxkms",
"i-slint-backend-winit",
"i-slint-common",
"i-slint-common 1.14.1",
"i-slint-core",
"i-slint-core-macros",
]
@ -3854,8 +3849,6 @@ dependencies = [
[[package]]
name = "i-slint-backend-winit"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69bf9167fb1165942ef1f034e039645b60b07b23c0c76069cb83595f979243a4"
dependencies = [
"bytemuck",
"cfg-if",
@ -3863,7 +3856,7 @@ dependencies = [
"copypasta",
"derive_more",
"futures",
"i-slint-common",
"i-slint-common 1.14.1",
"i-slint-core",
"i-slint-core-macros",
"i-slint-renderer-skia",
@ -3889,25 +3882,27 @@ dependencies = [
[[package]]
name = "i-slint-common"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3566194c13f8dcf6e9f41a2090c96f08cf3f59b60c91380a86c1ed72f6e7d19"
dependencies = [
"fontique",
"ttf-parser 0.25.1",
]
[[package]]
name = "i-slint-compiler"
name = "i-slint-common"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a301031f6c1da6acdd483641cc44109b34990e5edd67478eb0cbca359ab7ae3f"
checksum = "b3566194c13f8dcf6e9f41a2090c96f08cf3f59b60c91380a86c1ed72f6e7d19"
[[package]]
name = "i-slint-compiler"
version = "1.14.1"
dependencies = [
"by_address",
"codemap",
"codemap-diagnostic",
"derive_more",
"fontdue",
"i-slint-common",
"i-slint-common 1.14.1",
"image",
"itertools 0.14.0",
"linked_hash_set",
@ -3929,8 +3924,6 @@ dependencies = [
[[package]]
name = "i-slint-core"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc140f1218cfc4451b9e8753306c42afbcaf0386cc888e53664c1a5f5330ae19"
dependencies = [
"auto_enums",
"bitflags 2.10.0",
@ -3942,7 +3935,7 @@ dependencies = [
"derive_more",
"euclid",
"fontdue",
"i-slint-common",
"i-slint-common 1.14.1",
"i-slint-core-macros",
"image",
"integer-sqrt",
@ -3977,8 +3970,6 @@ dependencies = [
[[package]]
name = "i-slint-core-macros"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4c6a3975ccaa66415f5524292750e631879e69178aa97e3928d2396b790d00d"
dependencies = [
"quote",
"serde_json",
@ -3988,8 +3979,6 @@ dependencies = [
[[package]]
name = "i-slint-renderer-skia"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e26ed0d42d66c457b7937d7f5ee07d2fbef3691c896a26212a69b30288844ddd"
dependencies = [
"bytemuck",
"cfg-if",
@ -3998,7 +3987,7 @@ dependencies = [
"derive_more",
"glow",
"glutin",
"i-slint-common",
"i-slint-common 1.14.1",
"i-slint-core",
"i-slint-core-macros",
"lyon_path",
@ -6745,8 +6734,6 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "slint"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c35c4bdca2c42c69b21ceb416aa4ba76c3f54df30e9ce85dcad0742229422a6"
dependencies = [
"const-field-offset",
"i-slint-backend-selector",
@ -6776,8 +6763,6 @@ dependencies = [
[[package]]
name = "slint-macros"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b4b4bada19852716991153ddd93bf133548ac74383a05db8df399b9003bbfe"
dependencies = [
"i-slint-compiler",
"proc-macro2",
@ -7685,8 +7670,6 @@ dependencies = [
[[package]]
name = "vtable"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "753be81c38dff787d177b5939af1fa16f72f0d0d21a6b7d74ae56e29cd26f2a6"
dependencies = [
"const-field-offset",
"portable-atomic",
@ -7697,8 +7680,6 @@ dependencies = [
[[package]]
name = "vtable-macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cfcf6171aa2b0f85718ca5888ca32f6edf61d1849f8e4b3786ad890e5b68f68"
dependencies = [
"proc-macro2",
"quote",

View file

@ -68,6 +68,7 @@ itertools = { version = "0.14.0", default-features = false }
bytemuck = "1.24.0"
slint = { version = "1.14.1", default-features = false, features = ["compat-1-2", "libm", "log", "unsafe-single-threaded", "renderer-software"]}
i-slint-common = "1.14.1"
i-slint-core = { version = "1.14.1", default-features = false }
critical-section = "1.2.0"
cfg-if = "1.0.4"
xkbcommon = { git = "https://github.com/Limeth/xkbcommon-rs", branch = "esp32s3", default-features = false, features = ["c-lib-wrap"] }
@ -86,6 +87,8 @@ serde = { version = "1.0", default-features = false, features = ["derive"] }
# serde_with = { version = "3.16", default-features = false, features = ["alloc", "macros"] }
serde_bytes = { version = "0.11.19", default-features = false, features = ["alloc"] }
chrono = { version = "0.4.43", default-features = false, features = ["alloc", "serde"] } # TODO: defmt
tinyvec = { version = "1.10.0", default-features = false, features = ["alloc"] }
esp-metadata-generated = { version = "0.3.0", features = ["esp32s3"] }
# Crates for serial UART CLI
embedded-cli = { version = "0.2.1", default-features = false, features = ["help", "macros"] }

View file

@ -4,12 +4,11 @@ use core::alloc::GlobalAlloc;
use core::ffi::{c_size_t, c_void};
use enumset::EnumSet;
use esp_alloc::EspHeap;
use crate::ffi::string::__xkbc_memcpy;
// Here we select the allocator to use for libxkbcommon.
static XKBC_ALLOCATOR: &EspHeap = &crate::PSRAM_ALLOCATOR;
pub use crate::PSRAM_ALLOCATOR as XKBC_ALLOCATOR;
// Implementation based on esp-alloc's `compat` feature.

View file

@ -30,8 +30,8 @@ use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel;
use embassy_sync::mutex::Mutex;
use embassy_sync::signal::Signal;
use embassy_time::Duration;
use esp_alloc::{HeapRegion, MemoryCapability};
use embassy_time::{Duration, Timer};
use esp_alloc::{HeapRegion, MemoryCapability, RegionStats};
use esp_hal::Blocking;
use esp_hal::clock::CpuClock;
use esp_hal::dma::{BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig};
@ -159,7 +159,11 @@ async fn main(_spawner: Spawner) {
// Use the internal DRAM as the heap.
// Memory reclaimed from the esp-idf bootloader.
const HEAP_SIZE_RECLAIMED: usize = 72 * 1024;
const HEAP_SIZE_RECLAIMED: usize = const {
let range = esp_metadata_generated::memory_range!("DRAM2_UNINIT");
range.end - range.start
};
esp_alloc::heap_allocator!(#[ram(reclaimed)] size: HEAP_SIZE_RECLAIMED);
esp_alloc::heap_allocator!(size: HEAP_SIZE - HEAP_SIZE_RECLAIMED);
info!("Heap initialized! {:#?}", esp_alloc::HEAP.stats());
@ -424,6 +428,7 @@ async fn main(_spawner: Spawner) {
// TODO: Probably want to select! instead and re-try.
join_all![
run_alloc_stats_reporter(),
// We currently send the framebuffer data using the main core, which does not seem to slow
// down the rest of the tasks too much.
run_lcd(st7701s, framebuffer),
@ -447,6 +452,34 @@ async fn main(_spawner: Spawner) {
.await;
}
async fn run_alloc_stats_reporter() {
let mut psram_used_prev = 0;
let mut heap_used_prev = 0;
loop {
let psram_stats = PSRAM_ALLOCATOR.stats();
let heap_stats = esp_alloc::HEAP.stats();
if psram_stats.current_usage != psram_used_prev {
let difference = psram_stats.current_usage as isize - psram_used_prev as isize;
psram_used_prev = psram_stats.current_usage;
warn!(
"PSRAM usage changed: {}{}\n{psram_stats}",
if difference < 0 { '-' } else { '+' },
difference.abs()
);
}
if heap_stats.current_usage != heap_used_prev {
let difference = heap_stats.current_usage as isize - heap_used_prev as isize;
heap_used_prev = heap_stats.current_usage;
warn!(
"HEAP usage changed: {}{}\n{heap_stats}",
if difference < 0 { '-' } else { '+' },
difference.abs()
);
}
Timer::after_secs(1).await;
}
}
struct UserController {
sub: ControllerSub,
}
@ -551,7 +584,7 @@ async fn run_lcd(mut st7701s: St7701s<'static, Blocking>, framebuffer: &'static
// TODO: Use bounce buffers:
// https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer
// They need to be implemented in esp-hal.
// This can be implemented as a `DmaTxBuffer`.
let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) {
Err((error, result_dpi, result_dma_buf)) => {
error!(

View file

@ -107,6 +107,19 @@ pub async fn spi_read(
use crate::peripherals::st7701s::*;
// struct InitSequenceAction {
// command: ArrayCommand<8>,
// sleep: u64,
// }
// pub const INIT_SEQUENCE: [InitSequenceAction; _] = [CmdCn2bkxsel(
// CmdCn2bkxselArg0::new(),
// CmdCn2bkxselArg1::new(),
// CmdCn2bkxselArg2::new(),
// CmdCn2bkxselArg3::new(),
// CmdCn2bkxselArg4::new().with_bksel(0x3).with_cn2(true),
// )];
lazy_static! {
pub static ref INIT_SEQUENCE_COMMANDS: Vec<(Vec<Box<dyn Command + Send + Sync>>, u64)> = vec![
(vec![

View file

@ -3,13 +3,15 @@ use esp_hal::{
DriverMode,
gpio::{Flex, Level, Output},
lcd_cam::lcd::{
ClockMode, DelayMode, Phase, Polarity, dpi::{Dpi, Format, FrameTiming}
ClockMode, DelayMode, Phase, Polarity,
dpi::{Dpi, Format, FrameTiming},
},
time::Rate,
};
use lcd::spi_write;
use log::debug;
use paste::paste;
// use tinyvec::ArrayVec;
mod lcd;
@ -18,6 +20,30 @@ pub trait Command {
fn args_iter(&'_ self) -> core::slice::Iter<'_, u8>;
}
// pub struct ArrayCommand<const N: usize> {
// pub address: u8,
// pub args: ArrayVec<[u8; N]>,
// }
// impl<const N: usize> const ArrayCommand<N> {
// const fn from(command: impl Command) -> Self {
// Self {
// address: command.address(),
// args: command.args_iter().copied().collect(),
// }
// }
// }
// impl<const N: usize> Command for ArrayCommand<N> {
// fn address(&self) -> u8 {
// self.address
// }
// fn args_iter(&'_ self) -> core::slice::Iter<'_, u8> {
// self.args.iter()
// }
// }
pub struct CustomCommand {
pub address: u8,
pub args: &'static [u8],

View file

@ -31,8 +31,6 @@ impl slint::platform::Platform for SlintBackend {
fn create_window_adapter(
&self,
) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
// TODO: Custom window adapter impl needs to be implemented, so we can change `rotation` on
// `SoftwareRenderer`.
let renderer = SoftwareRenderer::new_with_repaint_buffer_type(
RepaintBufferType::ReusedBuffer, /* TODO: Implement a swapchain */
);

View file

@ -40,7 +40,7 @@ use spectre_api_sys::{
#[cfg(feature = "limit-fps")]
use crate::FRAME_DURATION_MIN;
use crate::{
SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER,
PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER,
db::{
AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers,
PartitionAcid, ReadTransactionExt,
@ -187,95 +187,99 @@ fn spectre_derive_user_key(username: &CStr, password: &CStr) -> SpectreUserKey {
pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) {
let db = AcidDatabase::mount(flash_part_acid).await;
let value = SpectreUsersConfig {
users: vec![SpectreUserConfig {
username: "test".to_string(),
encrypted_key: [0; _],
key_id: [0; _],
}],
};
// let value = SpectreUsersConfig {
// users: vec![SpectreUserConfig {
// username: "test".to_string(),
// encrypted_key: [0; _],
// key_id: [0; _],
// }],
// };
{
let mut write = db.write_transaction().await;
write
.write(
&DbKey::new(DbPathSpectreUserSite {
user_sites: DbPathSpectreUserSites {
username: "test".into(),
},
site: "example.org".into(),
}),
&postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
)
.await
.unwrap();
write
.write(
&DbKey::new(DbPathSpectreUserSite {
user_sites: DbPathSpectreUserSites {
username: "test".into(),
},
site: "sub.example.org".into(),
}),
&postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
)
.await
.unwrap();
write
.write(
&DbKey::new(DbPathSpectreUsers),
&postcard::to_allocvec(&value).unwrap(),
)
.await
.unwrap();
write.commit().await.unwrap();
}
// {
// let mut write = db.write_transaction().await;
// write
// .write(
// &DbKey::new(DbPathSpectreUserSite {
// user_sites: DbPathSpectreUserSites {
// username: "test".into(),
// },
// site: "example.org".into(),
// }),
// &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
// )
// .await
// .unwrap();
// write
// .write(
// &DbKey::new(DbPathSpectreUserSite {
// user_sites: DbPathSpectreUserSites {
// username: "test".into(),
// },
// site: "sub.example.org".into(),
// }),
// &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
// )
// .await
// .unwrap();
// write
// .write(
// &DbKey::new(DbPathSpectreUsers),
// &postcard::to_allocvec(&value).unwrap(),
// )
// .await
// .unwrap();
// write.commit().await.unwrap();
// }
let read_value = {
let read = db.read_transaction().await;
// TODO: https://github.com/embassy-rs/ekv/issues/20
let mut buffer = vec![0; 256];
let slice = read
.read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer)
.await
.unwrap();
let read_value = postcard::from_bytes::<SpectreUsersConfig>(&slice).unwrap();
// let read_value = {
// let read = db.read_transaction().await;
// // TODO: https://github.com/embassy-rs/ekv/issues/20
// let mut buffer = vec![0; 256];
// let slice = read
// .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer)
// .await
// .unwrap();
// let read_value = postcard::from_bytes::<SpectreUsersConfig>(&slice).unwrap();
let key_sites = DbKey::new(DbPathSpectreUserSites {
username: "test".into(),
});
let mut key_buffer = [0_u8; ekv::config::MAX_KEY_SIZE];
let mut cursor = read
.read_range(key_sites.range_of_children())
.await
.unwrap();
// let key_sites = DbKey::new(DbPathSpectreUserSites {
// username: "test".into(),
// });
// let mut key_buffer = [0_u8; ekv::config::MAX_KEY_SIZE];
// let mut cursor = read
// .read_range(key_sites.range_of_children())
// .await
// .unwrap();
while let Some((key_len, value_len)) =
cursor.next(&mut key_buffer, &mut buffer).await.unwrap()
{
let key = DbKey::from_raw(key_buffer[..key_len].into());
let mut key_segments = key.segments();
let value = &buffer[..value_len];
let site_config = postcard::from_bytes::<SpectreSiteConfig>(&value).unwrap();
let site = SpectreSite {
config: site_config,
username: key_segments.nth(2).unwrap().into(),
site_name: key_segments.nth(1).unwrap().into(),
};
// while let Some((key_len, value_len)) =
// cursor.next(&mut key_buffer, &mut buffer).await.unwrap()
// {
// let key = DbKey::from_raw(key_buffer[..key_len].into());
// let mut key_segments = key.segments();
// let value = &buffer[..value_len];
// let site_config = postcard::from_bytes::<SpectreSiteConfig>(&value).unwrap();
// let site = SpectreSite {
// config: site_config,
// username: key_segments.nth(2).unwrap().into(),
// site_name: key_segments.nth(1).unwrap().into(),
// };
info!("site = {:#?}", site);
}
// info!("site = {:#?}", site);
// }
read_value
};
// read_value
// };
info!("read_value = {:#?}", read_value);
assert_eq!(value, read_value, "values do not match");
// info!("read_value = {:#?}", read_value);
// assert_eq!(value, read_value, "values do not match");
// TODO:
// * Store a config as a versioned postcard-serialized struct
// * Store accounts and sites as ranges in the DB
i_slint_core::properties::ALLOCATOR
.set(&PSRAM_ALLOCATOR)
.ok()
.unwrap();
slint::platform::set_platform(Box::new(backend)).expect("backend already initialized");
struct State {
@ -432,6 +436,7 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
.into()
};
warn!("Identicon: {identicon} ({identicon:?})");
state.window.set_user_edit_identicon(identicon.clone());
state.state_user_edit.hashed = Some(ProposedPassword {
encrypted_key,

View file

@ -28,7 +28,7 @@ export enum AppState {
export component AppWindow inherits Window {
// Special characters to generate pre-render glyphs for.
in property <string> dummy: "ÄÖÜÁÉÍÓÚÝŔŚĹŹĆŃĚĽŽŠČŘĎŤŇŮÅäöüáéíóúýŕśĺźćńěľžščřďťňůåß„“”‘’—–@&$%+=¡¿¢£$¥€²³¼½¬¤¦§©®™°´ˇ¨";
// in property <string> dummy_identicon_symbols: "╔╚╰═█░▒▓☺☻╗╝╯═◈◎◐◑◒◓☀☁☂☃☄★☆☎☏⎈⌂☘☢☣☕⌚⌛⏰⚡⛄⛅☔♔♕♖♗♘♙♚♛♜♝♞♟♨♩♪♫⚐⚑⚔⚖⚙⚠⌘⏎✄✆✈✉✌";
in property <string> dummy_identicon_symbols: "╔╚╰═█░▒▓☺☻╗╝╯═◈◎◐◑◒◓☀☁☂☃☄★☆☎☏⎈⌂☘☢☣☕⌚⌛⏰⚡⛄⛅☔♔♕♖♗♘♙♚♛♜♝♞♟♨♩♪♫⚐⚑⚔⚖⚙⚠⌘⏎✄✆✈✉✌";
default-font-family: "IBM Plex Mono";
default-font-size: 16pt;
height: 368px;