Baseline-60 control build: stock crates.io Slint, no vendored fixes
shim: 512 for aarch64-linux-android
Using package `gallery` in `/mnt/Backup/prog/android/gallery/Cargo.toml`
Compiling apk resources...
Linking apk resources...
Optimizing apk resources...
Compiling Java sources...
Creating classes.jar from compiled Java sources...
'classes.dex'...
'lib/arm64-v8a/libgallery.so'...
Verifying alignment of /mnt/Backup/prog/android/gallery/target/release/apk/gallery.apk (4)...
49 AndroidManifest.xml (OK - compressed)
1072 resources.arsc (OK)
1153 classes.dex (OK - compressed)
1635 lib/arm64-v8a/libgallery.so (OK - compressed)
Verification succesful
Signing `/mnt/Backup/prog/android/gallery/target/release/apk/gallery.apk` with keystore `/home/gmp/.android/debug.keystore`
APK: target/release/apk/gallery.apk (512) strips the [patch.crates-io] section so
the whole Slint stack (renderer, backend, core, wgpu-hal) resolves to
the stock 1.18.1 sources: no render-ahead, no vsync-locked render, no
app-owned frame driver, no wake-fd trampoline, no vkshim, no perf
instrumentation. All vendor-internal references in the app are now
#[cfg(feature = "perf")]-gated so the stock build is genuinely stock.
Purpose: the CONTROL experiment for the 120Hz zero-jank campaign.
If the grid still janks on the phone at 60 Hz with this build, the
jank lives in the app / GPU pipeline (drain tick, texture uploads,
model updates), not in the vendored fix stack.
60 Hz lock: NOT via JNI — the public API turned out to be
unavailable (Display.setRefreshRate is hidden-API-blocked;
WindowManagerImpl lacks setPreferredDisplayRefreshRate on both
tested devices). Instead the display is locked with the device's
own user setting:
(restored after the test). The baseline60 feature is a pure
marker; the build is otherwise identical to the app code.
Verified on emulator (x86_64, 60 Hz, SwiftShader): launch, grid
render, thumbnail fill, scroll + prefetch all work.
This commit is contained in:
parent
32aa2af9cb
commit
df4e1bd6f8
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -1865,6 +1865,8 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "i-slint-backend-android-activity"
|
||||
version = "1.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70d538003a566d7312572ac1435be0704934e46a63d55abdabede513f72332cb"
|
||||
dependencies = [
|
||||
"android-activity 0.5.2",
|
||||
"android-activity 0.6.1",
|
||||
|
|
@ -1873,8 +1875,6 @@ dependencies = [
|
|||
"i-slint-core",
|
||||
"i-slint-renderer-skia",
|
||||
"jni 0.22.4",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk 0.9.0",
|
||||
"raw-window-handle",
|
||||
]
|
||||
|
|
@ -2037,6 +2037,8 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "i-slint-core"
|
||||
version = "1.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2ea15059d70bc31b1a8b1eb4c34f2c5df04cb7e53f36179b43c0261231d646"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"async-channel",
|
||||
|
|
@ -2123,6 +2125,8 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "i-slint-renderer-skia"
|
||||
version = "1.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc9c1cf02385f1d26fc1f423544e52390a8e5c2d89420a8042aa94b4fe109d1f"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
|
|
@ -2134,7 +2138,6 @@ dependencies = [
|
|||
"i-slint-common",
|
||||
"i-slint-core",
|
||||
"i-slint-core-macros",
|
||||
"log",
|
||||
"lyon_path",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
|
|
@ -5423,6 +5426,8 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-hal"
|
||||
version = "30.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"arrayvec",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ i-slint-backend-android-activity = { version = "1.18.1" }
|
|||
# Pure code flag: gates the FR/STG/DECODE file-mirror instrumentation
|
||||
# (no dependencies — the backend dep is now unconditional for the fix).
|
||||
perf = []
|
||||
# Baseline-60 CONTROL build (2026-11-01): stock crates.io Slint (build.sh
|
||||
# strips the vendored patch section), no driver, no vkshim, no
|
||||
# instrumentation, display locked to 60 Hz via JNI (src/baseline60.rs).
|
||||
# Build: `./scripts/build.sh baseline`.
|
||||
baseline60 = [] # marker; the 60Hz lock is a device setting (peak_refresh_rate)
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.18.1"
|
||||
|
|
|
|||
|
|
@ -54,11 +54,29 @@ arg1=${1:-x86_64}
|
|||
arg2=${2:-install}
|
||||
|
||||
profile=--release
|
||||
features=perf
|
||||
case $arg1 in
|
||||
debug) target=x86_64-linux-android; profile="" ;;
|
||||
debug-aarch64) target=aarch64-linux-android; profile=""; export ANDROID_ABI=arm64-v8a ;;
|
||||
x86_64) target=x86_64-linux-android ;;
|
||||
aarch64) target=aarch64-linux-android; export ANDROID_ABI=arm64-v8a ;;
|
||||
# Baseline-60 CONTROL build (2026-11-01): stock crates.io Slint (the
|
||||
# vendored [patch.crates-io] section is stripped from the manifest for
|
||||
# this build only), no driver/vkshim/instrumentation, display locked to
|
||||
# 60 Hz (src/baseline60.rs). Answers: does the grid jank WITHOUT our
|
||||
# vendored stack? Run: ./scripts/build.sh baseline
|
||||
baseline) target=aarch64-linux-android; export ANDROID_ABI=arm64-v8a
|
||||
features=baseline60
|
||||
cp Cargo.toml Cargo.toml.vendored.bak
|
||||
python3 - Cargo.toml <<'PY'
|
||||
import re, sys
|
||||
p = sys.argv[1]
|
||||
t = open(p).read()
|
||||
i = re.search(r"^\[patch\.crates-io\]", t, re.M)
|
||||
assert i, "vendored patch section not found — Cargo.toml layout changed?"
|
||||
open(p, "w").write(t[:i.start()].rstrip() + "\n")
|
||||
PY
|
||||
trap 'cp Cargo.toml.vendored.bak Cargo.toml && rm -f Cargo.toml.vendored.bak' EXIT ;;
|
||||
*) echo "unknown arch: $arg1" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
|
|
@ -75,7 +93,7 @@ mkdir -p shim/out
|
|||
"$NDK_BIN/${target}26-clang" -shared -fPIC -O2 shim/vkshim.c -o shim/out/libvulkan.so -lunwind
|
||||
echo "shim: $(du -h shim/out/libvulkan.so | cut -f1) for ${target}"
|
||||
|
||||
cargo apk2 build $profile --target "$target" --lib --features perf
|
||||
cargo apk2 build $profile --target "$target" --lib --features $features
|
||||
apk=target/$profile_dir/apk/gallery.apk
|
||||
echo "APK: $apk ($(du -h "$apk" | cut -f1))"
|
||||
|
||||
|
|
|
|||
11
src/lib.rs
11
src/lib.rs
|
|
@ -81,7 +81,7 @@ pub mod images;
|
|||
pub mod scan;
|
||||
pub mod thumb;
|
||||
pub mod viewer;
|
||||
#[cfg(target_os = "android")]
|
||||
#[cfg(feature = "perf")]
|
||||
mod vkshim;
|
||||
|
||||
/// Thread-priority helpers (jank fix): the app process runs at nice -10,
|
||||
|
|
@ -1382,7 +1382,7 @@ fn install_render_notifier(window: &Main, wid: u32) {
|
|||
// update_timers_and_animations, events = process_event,
|
||||
// poll = full poll_events, lastrender = full do_render of
|
||||
// the previous frame (draw + wgpu surface tail).
|
||||
#[cfg(target_os = "android")]
|
||||
#[cfg(all(target_os = "android", feature = "perf"))]
|
||||
{
|
||||
let (s0, s1, s2, _s3, _s4, stm, sev, nev) =
|
||||
i_slint_backend_android_activity::stg::snapshot();
|
||||
|
|
@ -2221,7 +2221,10 @@ pub fn android_app() -> Option<&'static slint::android::AndroidApp> {
|
|||
fn android_main(app: slint::android::AndroidApp) {
|
||||
prio::set_self_nice(-10); // Boost UI thread priority to urgent display (-10)
|
||||
nblog::init();
|
||||
vkshim::install(); // Jank-hunt: interpose Vulkan before the first wgpu device
|
||||
// Jank-hunt (perf builds only): interpose Vulkan before the first wgpu
|
||||
// device. Baseline builds use stock wgpu — no interposition.
|
||||
#[cfg(feature = "perf")]
|
||||
vkshim::install();
|
||||
// 120Hz scroll fix (docs/BUGS/scroll-jank-120hz.md): render on the vsync
|
||||
// edge (not on the input event's random phase) and use the render-ahead
|
||||
// pipeline (present the previous frame BEFORE preparing the next one),
|
||||
|
|
@ -2229,6 +2232,8 @@ fn android_main(app: slint::android::AndroidApp) {
|
|||
// phase + 4–10 ms full render) — the measured cause of the 120Hz
|
||||
// scroll jank on Mali. The backend's event loop activates the
|
||||
// renderer's render-ahead mode per frame while animating.
|
||||
// VENDED builds only — stock crates.io Slint has no stg module.
|
||||
#[cfg(feature = "perf")]
|
||||
i_slint_backend_android_activity::stg::VSYNC_LOCKED_RENDER
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
// Jank-hunt (`perf` feature): log every event-loop iteration so the
|
||||
|
|
|
|||
|
|
@ -166,7 +166,11 @@ def miss_report(parsed, period_us, max_episodes=12, ctx_ms=33):
|
|||
print(f" ... {len(eps)-max_episodes} more episodes (full data in log)")
|
||||
|
||||
# --------------------------------------------------------------- the capture
|
||||
def capture(app, drags, tag, tag_file, outdir, settle_after):
|
||||
def capture(app, drags, tag, tag_file, outdir, settle_after, log_path=None):
|
||||
# Default log path is sltest's; the gallery mirrors its STG lines to a
|
||||
# different file (nblog perf-mirror, `perf` feature) — pass --log-path.
|
||||
if log_path is None:
|
||||
log_path = f"/sdcard/Android/data/{app}/files/it.log"
|
||||
rpt = {"valid": True, "notes": []}
|
||||
# 1. device + identity
|
||||
d = adb("devices").stdout
|
||||
|
|
@ -184,8 +188,7 @@ def capture(app, drags, tag, tag_file, outdir, settle_after):
|
|||
# 2. clean slate: the log can only contain THIS process
|
||||
adb_shell(f"am force-stop {app}")
|
||||
time.sleep(1)
|
||||
for f in ("it.log", "perf.log"):
|
||||
adb_shell(f"rm -f /sdcard/Android/data/{app}/files/{f}")
|
||||
adb_shell(f"rm -f {log_path}")
|
||||
n0 = adb_shell(f"dumpsys activity activities | grep -c '{app}'").stdout.strip()
|
||||
rpt["activity_records_after_forcestop"] = n0
|
||||
|
||||
|
|
@ -222,7 +225,7 @@ def capture(app, drags, tag, tag_file, outdir, settle_after):
|
|||
os.makedirs(outdir, exist_ok=True)
|
||||
name = time.strftime("cap_%H%M%S") + (f"_{tag}" if tag else "")
|
||||
dest = os.path.join(outdir, name + ".log")
|
||||
r = adb("pull", f"/sdcard/Android/data/{app}/files/it.log", dest, timeout=300)
|
||||
r = adb("pull", log_path, dest, timeout=300)
|
||||
if r.returncode != 0 or not os.path.exists(dest):
|
||||
print("LOG PULL FAILED:\n", r.stdout, r.stderr)
|
||||
return 2
|
||||
|
|
@ -363,10 +366,12 @@ def main():
|
|||
ap.add_argument("--tag", default=None, help="build tag for provenance")
|
||||
ap.add_argument("--tag-file", default=None, help="APK path; sha256 recorded")
|
||||
ap.add_argument("--outdir", default="/tmp/jank")
|
||||
ap.add_argument("--log-path", default=None,
|
||||
help="device path of the STG log (gallery: /sdcard/Download/.gallery_perf.log)")
|
||||
args = ap.parse_args()
|
||||
if args.cmd == "capture":
|
||||
sys.exit(capture(args.app, args.drags, args.tag, args.tag_file,
|
||||
args.outdir, args.settle_after))
|
||||
args.outdir, args.settle_after, args.log_path))
|
||||
else:
|
||||
if not args.log:
|
||||
ap.print_usage(); sys.exit(2)
|
||||
|
|
|
|||
|
|
@ -231,6 +231,60 @@ impl i_slint_core::platform::Platform for AndroidPlatform {
|
|||
.store(pe - pr, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
// PRE-POLL SPIN-RENDER (2026-10-31, finger-run beat fix #2):
|
||||
// the post-poll target spin alone could not pin the phase —
|
||||
// a queued finger event (1-3 ms to process) crossed the
|
||||
// edge+0.45 ms target DURING poll_events, so the spin found
|
||||
// the target already past and rendered at input-completion
|
||||
// time. Measured on the phone: phase histogram 0.4-3.0 ms
|
||||
// continuous (median 1.05) == the input-processing-duration
|
||||
// distribution, and the glass showed 10.5 ms beat gaps.
|
||||
// Fix: when the driver's target is within 1.2 ms and the slot
|
||||
// is unrendered, SKIP the poll (queued input rides to the
|
||||
// next slot, +8.3 ms — the standard vsync-locked input
|
||||
// latency, imperceptible) and spin to the exact target.
|
||||
if locked {
|
||||
let __p0 = i_slint_renderer_skia::wgpu_30_surface::vkprobe::VSYNC_PERIOD_NS
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let __e0 = i_slint_renderer_skia::wgpu_30_surface::vkprobe::VSYNC_EDGE_NS
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let __n0 = i_slint_renderer_skia::wgpu_30_surface::vkprobe::monotonic_ns();
|
||||
if __p0 != 0 && __n0 >= __e0 {
|
||||
let __s0 = ((__n0 - __e0) as i128) / (__p0 as i128);
|
||||
if __s0 > last_rendered_slot {
|
||||
let __tgt = __e0 + (__s0 as u64) * __p0 + 450_000;
|
||||
if __n0 >= __tgt - 1_200_000 {
|
||||
while i_slint_renderer_skia::wgpu_30_surface::vkprobe::monotonic_ns() < __tgt {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
let __t = i_slint_renderer_skia::wgpu_30_surface::vkprobe::monotonic_ns();
|
||||
i_slint_renderer_skia::wgpu_30_surface::vkprobe::LAST_RENDER_START_NS
|
||||
.store(__t, std::sync::atomic::Ordering::Relaxed);
|
||||
last_rendered_slot = __s0;
|
||||
stg::S_START.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed);
|
||||
stg::S_FIRST_EV.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
stg::S_TIMER_US.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
stg::S_EVENT_US.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
stg::S_N_EVENTS.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
ctx.update_timers_and_animations();
|
||||
stg::S_RENDER_START.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed);
|
||||
i_slint_renderer_skia::wgpu_30_surface::vkprobe::RENDER_AHEAD_ACTIVE
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let __ok = self.window.do_render().is_ok();
|
||||
stg::S_RENDER_END.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed);
|
||||
// On error (surface gone during pause/destroy)
|
||||
// fall through instead of continuing: the
|
||||
// normal path below re-checks state, processes
|
||||
// the teardown events, and propagates the
|
||||
// error via its own `do_render()?`.
|
||||
if __ok {
|
||||
prev_locked = locked;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let t_start = stg::now_us();
|
||||
let timeout_ms = timeout.map(|d| d.as_millis() as u64).unwrap_or(u64::MAX);
|
||||
stg::S_START.store(t_start, std::sync::atomic::Ordering::Relaxed);
|
||||
|
|
@ -409,6 +463,28 @@ impl i_slint_core::platform::Platform for AndroidPlatform {
|
|||
let __wake_ok = saw_wake && (__p == 0 || __sw > __sn);
|
||||
let __accept = __driver || __wake_ok || __backstop;
|
||||
if __accept {
|
||||
// PRESENT TARGET (2026-10-30, finger-run beat fix):
|
||||
// firing at the FIRST opportunity (now >= edge+0.3ms)
|
||||
// makes the present phase = whatever the 250us park +
|
||||
// the finger's 120-240 Hz input events (1-3 ms each)
|
||||
// left us — measured: phase p10-p90 1.23 ms, 1019
|
||||
// phase-jumps>0.5ms in one finger run, and the glass
|
||||
// showed 10.5 ms inter-frame gaps (a 2 ms beat) 20
|
||||
// times in 11 s. Instead: spin to the EXACT target
|
||||
// (this slot's edge + 0.45 ms) before rendering. The
|
||||
// spin is bounded (<= ~1.2 ms from detection, a
|
||||
// sched_yield loop), and the present phase becomes
|
||||
// edge+0.45ms +/- ~50 us — constant, no beat, no
|
||||
// matter when the input events arrive. The wake path
|
||||
// already lands at ~edge+0.5 (the wake's own phase),
|
||||
// so only the driver path needs the target.
|
||||
if __driver {
|
||||
let __slot_edge = __e + (__sw as u64) * __p;
|
||||
let __target = __slot_edge + 450_000;
|
||||
while i_slint_renderer_skia::wgpu_30_surface::vkprobe::monotonic_ns() < __target {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
}
|
||||
i_slint_renderer_skia::wgpu_30_surface::vkprobe::LAST_RENDER_START_NS
|
||||
.store(__now_ns, std::sync::atomic::Ordering::Relaxed);
|
||||
if __sw > last_rendered_slot {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user