Slint-on-Mali was conclusively janky at its base level (sltest: 0.33-0.83% slot drops on a scene with no app logic); egui on the same phone measured 3-7x cleaner (egtest: 0.12%, same referee/scene). Architecture (see HANDOFF §8.11-8.12): - winit 0.30 ApplicationHandler on android-activity 0.6, raw EGL (ES3), egui_glow::EguiGlow. winit 0.30 never delivers RedrawRequested on Android → the frame driver is app-owned: World::tick runs every loop iteration from about_to_wait (liveness), render when dirty or animating, park ~8 ms (one vblank) at rest. - Virtualized grid painter (3 cols, visible rows ±4), GPU-resident textures, turbojpeg worker pool decoding off-thread. - CRITICAL pixel-pipeline fix: RGB8→Vec<Color32> conversion happens on the WORKER (egui 0.31's from_rgba_unmultiplied is O(n) per-pixel, 8-60 ms/2 MB on the UI thread → 9.57% drops + phone OOM crash). UI thread only MOVES the vector into ColorImage. - Fling: per-event EMA velocity (frame-based dies at slow touch sampling), clamp 14000 px/s; decay exp(-7.42 dt). - Painter::image uv is NORMALIZED (0..1) — a screen-space uv made every cell a solid rectangle (the emulator's solid-color test images hid it). - Memory caps for a personal phone: full cache 6 entries / 16 MP. Verified on emulator: grid, scroll, fling, viewer, back-key. Phone deploy: thumbnails render, 8580 files. Pending: user finger test + fr_capture phone run (per-pixel fix expected to cut render cost 18.8 ms → <8.3 ms). Deletes: vendor/ (instrumented Slint tree), shim/, build.rs, gputex, vkshim, baseline60, main.slint — the Slint zero-jank campaign is concluded in docs/BUGS/.
114 lines
4.1 KiB
Rust
114 lines
4.1 KiB
Rust
//! Non-blocking logger (Android). `log` calls on the UI thread must NEVER
|
||
//! block: logd-pipe backpressure during bursts was observed to block the
|
||
//! UI thread 10–40 ms (docs/BUGS/scroll-jank-120hz.md). Records are
|
||
//! formatted at the call site, pushed into a bounded channel; a drain
|
||
//! thread does the real `__android_log_write` and DROPS records when
|
||
//! saturated.
|
||
//!
|
||
//! `framelog`: FR lines are also mirrored to
|
||
//! /sdcard/Download/.gallery_perf.log — the logcat ring buffer rotates
|
||
//! every few seconds, which destroys the gap analysis. tools/fr_capture.py
|
||
//! pulls this file.
|
||
|
||
#[cfg(target_os = "android")]
|
||
pub mod android {
|
||
use std::io::Write as _;
|
||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||
use std::sync::mpsc;
|
||
use log::{Level, Log, Metadata, Record};
|
||
|
||
extern "C" {
|
||
fn __android_log_write(prio: i32, tag: *const u8, msg: *const u8) -> i32;
|
||
}
|
||
const TAG: &[u8] = b"gallery\0";
|
||
const PERF_PATH: &str = "/sdcard/Download/.gallery_perf.log";
|
||
|
||
fn perf_line(msg: &str) -> bool {
|
||
msg.starts_with("FR ") || msg.starts_with("PERF ")
|
||
|| msg.starts_with("gallery: [W")
|
||
}
|
||
|
||
pub struct Logger {
|
||
tx: mpsc::SyncSender<(i32, String)>,
|
||
dropped: AtomicUsize,
|
||
}
|
||
|
||
pub fn init() {
|
||
let (tx, rx) = mpsc::sync_channel::<(i32, String)>(4096);
|
||
std::thread::Builder::new()
|
||
.name("log-drain".into())
|
||
.spawn(move || {
|
||
// Opened lazily (MANAGE_EXTERNAL_STORAGE may be granted
|
||
// later); retried until it works.
|
||
let mut perf: Option<std::io::BufWriter<std::fs::File>> = None;
|
||
let mut n = 0usize;
|
||
for (prio, msg) in rx {
|
||
if cfg!(feature = "framelog") && perf_line(&msg) {
|
||
if perf.is_none() {
|
||
if let Ok(f) = std::fs::OpenOptions::new()
|
||
.create(true)
|
||
.append(true)
|
||
.open(PERF_PATH)
|
||
{
|
||
perf = Some(std::io::BufWriter::new(f));
|
||
}
|
||
}
|
||
if let Some(w) = perf.as_mut() {
|
||
let _ = w.write_all(msg.as_bytes());
|
||
let _ = w.write_all(b"\n");
|
||
n += 1;
|
||
if n >= 32 {
|
||
let _ = w.flush();
|
||
n = 0;
|
||
}
|
||
}
|
||
}
|
||
let b = msg.into_bytes();
|
||
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||
let mut safe: Vec<u8> = b[..end].iter().copied().filter(|&c| c != b'\n').collect();
|
||
safe.push(0);
|
||
unsafe {
|
||
__android_log_write(prio, TAG.as_ptr(), safe.as_ptr());
|
||
}
|
||
}
|
||
})
|
||
.ok();
|
||
let logger = Logger { tx, dropped: AtomicUsize::new(0) };
|
||
let _ = log::set_boxed_logger(Box::new(logger)).map(|()| log::set_max_level(log::LevelFilter::Info));
|
||
}
|
||
|
||
impl Logger {
|
||
fn prio(l: Level) -> i32 {
|
||
match l {
|
||
Level::Error => 6,
|
||
Level::Warn => 5,
|
||
Level::Info => 4,
|
||
_ => 3,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Log for Logger {
|
||
fn enabled(&self, m: &Metadata) -> bool {
|
||
m.level() <= Level::Info
|
||
}
|
||
fn log(&self, record: &Record) {
|
||
if !self.enabled(record.metadata()) {
|
||
return;
|
||
}
|
||
let mut s = String::with_capacity(96);
|
||
use std::fmt::Write as _;
|
||
let _ = write!(&mut s, "{}", record.args());
|
||
if self.tx.try_send((Self::prio(record.level()), s)).is_err() {
|
||
self.dropped.fetch_add(1, Ordering::Relaxed);
|
||
}
|
||
}
|
||
fn flush(&self) {}
|
||
}
|
||
}
|
||
|
||
#[cfg(not(target_os = "android"))]
|
||
pub fn init() {
|
||
// Desktop: the binary installs its own minimal logger.
|
||
}
|