The A/B/C harness series: sltest (Slint) / egtest (egui) / tttest (Tauri 2 → wry → Android WebView/Chromium). Identical scene to sltest: 8580 full-width rows, 150 px + 2 px divider, alternating colors, virtualized (26-element DOM pool), custom touch drag + exponential-decay fling (sltest decay constant 7.42), rAF-driven frame loop. FR logging: the rAF callback records gap/cost exactly like the Rust harnesses; lines batch (~120) and cross the Tauri event bridge once per second (JNI round trip kept off the per-frame path). Rust side drains to a dedicated writer thread → /sdcard/Android/data/wow.gmp.tttest/files/Documents/fr.log (Tauri document_dir = getExternalFilesDir(DOCUMENTS) — the framework CREATES it; a raw std::fs mkdir of the app's external tree is SELinux-denied on Android 11+). Same FR-line format → fr_capture.py is the referee (its launch check now falls back to the package's LAUNCHER activity via monkey for non-NativeActivity apps). Gotchas learned (see frontend/dist/index.html comments): - touch-action is NOT inherited: touches land on .row, so each row must declare touch-action:none or the WebView claims the pan and cancels the page's drag (only a few px register). - Fling velocity must be a per-EVENT EMA; frame-based tracking dies when touch events sample slower than rAF (adb swipe ≈ 17 Hz). - Drag sign: finger up → offset grows (offset -= finger dy). - Tauri v2 'brownfield' is the NORMAL direct-IPC mode (not an app-provides-content mode); the mobile schema rejects security.pattern entirely. - Custom #[tauri::command]s hit the two-pass app-command ACL chicken-and-egg; the core:default event bridge avoids it. - Rust log output needs a logcat backend (log/std + __android_log_write) or it is invisible. Emulator (60 Hz, scripted swipes): 1319 frames, median 16.70 ms, 5 isolated drops (0.38%), all at swipe-injection moments, render cost med 2 ms. aarch64 phone build compiles (11 MB APK); phone testing pending (personal-device policy). Build: harnesses/tttest/scripts/build.sh [x86_64|aarch64] — cargo-tauri CLI (v2.12.4, prebuilt) drives the gradle build; signed with the debug keystore.
216 lines
6.5 KiB
HTML
216 lines
6.5 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
|
<title>tttest</title>
|
|
<style>
|
|
/* Full-screen, no native scroll of ANY kind: all motion is ours, on
|
|
the compositor layer (transform: translate3d). Mirrors sltest:
|
|
full-width rows, 150 px + 2 px white divider, alternating colors. */
|
|
html, body {
|
|
margin: 0; padding: 0;
|
|
position: fixed; inset: 0;
|
|
width: 100%; height: 100%;
|
|
overflow: hidden;
|
|
background: #0a0a0c;
|
|
touch-action: none;
|
|
overscroll-behavior: none;
|
|
-webkit-user-select: none; user-select: none;
|
|
}
|
|
#rows { position: absolute; left: 0; top: 0; width: 100%; height: 0; will-change: transform; }
|
|
.row {
|
|
position: absolute; left: 0; width: 100%; height: 150px;
|
|
will-change: transform;
|
|
/* touch-action is NOT inherited — touches land on .row, so each must
|
|
claim the gesture. Otherwise the WebView sees touch-action:auto at
|
|
the hit element and takes over the pan (page gets touchcancel),
|
|
leaving only a few px of drag registered. */
|
|
touch-action: none;
|
|
}
|
|
.row .div {
|
|
position: absolute; left: 0; bottom: 0; width: 100%; height: 2px;
|
|
background: #ffffff;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="rows"></div>
|
|
<script>
|
|
"use strict";
|
|
let frames = 0;
|
|
|
|
// ---- scene (sltest mirror) -------------------------------------------
|
|
const N = 8580; // rows (same as the big sltest scene)
|
|
const ROW = 150, DIV = 2, RH = ROW + DIV;
|
|
const W = () => window.innerWidth;
|
|
const H = () => window.innerHeight;
|
|
const MAXOFF = () => Math.max(0, N * RH - H());
|
|
const COLOR = [
|
|
[60, 90, 130], // blue
|
|
[130, 70, 90], // maroon
|
|
];
|
|
|
|
// ---- virtualization: a fixed pool of rows, repositioned per frame ----
|
|
const POOL = 26;
|
|
const wrap = document.getElementById("rows");
|
|
const pool = [];
|
|
for (let i = 0; i < POOL; i++) {
|
|
const d = document.createElement("div");
|
|
d.className = "row";
|
|
const v = document.createElement("div");
|
|
v.className = "div";
|
|
d.appendChild(v);
|
|
wrap.appendChild(d);
|
|
pool.push({ el: d, color: -1, y: -1 });
|
|
}
|
|
|
|
// ---- scroll state ------------------------------------------------------
|
|
let offset = 0; // content px scrolled (0 = top)
|
|
let dragging = false;
|
|
let dragY0 = 0, dragOff0 = 0;
|
|
let vel = 0; // content px/s (positive = scrolling down)
|
|
let lastT = 0, lastY = 0;
|
|
|
|
function clampOffset(o) {
|
|
if (o < 0) return 0;
|
|
const m = MAXOFF();
|
|
return o > m ? m : o;
|
|
}
|
|
|
|
function applyOffset() {
|
|
wrap.style.transform = "translate3d(0," + (-offset).toFixed(2) + "px,0)";
|
|
}
|
|
|
|
function paint() {
|
|
const h = H();
|
|
const first = Math.max(0, Math.floor(offset / RH) - 2);
|
|
const last = Math.min(N - 1, Math.ceil((offset + h) / RH) + 2);
|
|
for (let i = 0; i < POOL; i++) {
|
|
const p = pool[i];
|
|
const r = first + i;
|
|
if (r > last) { p.el.style.display = "none"; continue; }
|
|
p.el.style.display = "block";
|
|
if (r % 2 !== p.color) {
|
|
const c = COLOR[r % 2];
|
|
p.el.style.background = "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")";
|
|
p.color = r % 2;
|
|
}
|
|
const y = Math.round(r * RH * 100) / 100;
|
|
if (y !== p.y) {
|
|
p.el.style.transform = "translate3d(0," + y + "px,0)";
|
|
p.y = y;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- FR logging (fr_capture.py convention) -----------------------------
|
|
// The rAF timestamp is the Chromium render thread's high-res clock.
|
|
// gap = time since the PREVIOUS frame callback (the frame period the
|
|
// user sees); cost = this callback's own work. Batches ~1/s keep the
|
|
// Tauri IPC (a JNI round trip) at 1/s, off the per-frame path.
|
|
let frLast = -1;
|
|
let frBatch = [];
|
|
let frLines = 0;
|
|
function frLog(t) {
|
|
const work = performance.now() - t;
|
|
if (frLast >= 0) {
|
|
const gap = t - frLast;
|
|
frBatch.push("FR " + t.toFixed(2).padStart(11) +
|
|
" gap=" + gap.toFixed(2).padStart(7) +
|
|
" cost=" + work.toFixed(2).padStart(6) + " drew=1");
|
|
if (++frLines % 120 === 0) {
|
|
flushFr();
|
|
}
|
|
}
|
|
frLast = t;
|
|
}
|
|
function flushFr() {
|
|
if (frBatch.length === 0) return;
|
|
const payload = frBatch.join("\n") + "\n";
|
|
frBatch = [];
|
|
try {
|
|
window.__TAURI__.event.emit("tttest-fr", payload);
|
|
} catch (e) { /* pre-bridge (desktop) */ }
|
|
}
|
|
|
|
// ---- frame loop (runs ALWAYS; Chromium drives the rate) ----------------
|
|
function frame(t) {
|
|
frames++;
|
|
const t0 = performance.now();
|
|
if (dragging) {
|
|
// 1:1 finger follow (velocity is tracked per-EVENT in touchmove —
|
|
// frame-based EMA dies when touch events sample slower than rAF).
|
|
offset = clampOffset(dragOff0 - (yNow - dragY0));
|
|
lastT = t;
|
|
} else if (Math.abs(vel) > 0) {
|
|
const dt = Math.min(0.1, (t - lastT) / 1000);
|
|
offset = clampOffset(offset + vel * dt);
|
|
vel = vel * Math.exp(-7.42 * dt); // sltest/gallery decay (0.94 @120fps)
|
|
if (Math.abs(vel) < 1100) vel = 0;
|
|
lastT = t;
|
|
}
|
|
paint();
|
|
applyOffset();
|
|
frLog(t);
|
|
requestAnimationFrame(frame);
|
|
}
|
|
|
|
// ---- touch (pointer events; single finger) ------------------------------
|
|
let yNow = 0;
|
|
// Per-event velocity (the gallery's fixed estimator): delta between
|
|
// successive touch events ÷ time between them, EMA 0.8/0.2 (~60 ms
|
|
// memory at 120 Hz). Frame-based tracking breaks when events sample
|
|
// slower than rAF (adb swipes arrive ~17 Hz).
|
|
let evY = 0, evT = 0;
|
|
window.addEventListener("touchstart", (e) => {
|
|
const t = e.changedTouches[0];
|
|
yNow = t.clientY;
|
|
dragging = true;
|
|
dragY0 = yNow;
|
|
dragOff0 = offset;
|
|
vel = 0;
|
|
evY = yNow;
|
|
evT = performance.now();
|
|
lastT = performance.now();
|
|
e.preventDefault();
|
|
}, { passive: false });
|
|
|
|
window.addEventListener("touchmove", (e) => {
|
|
const t = e.changedTouches[0];
|
|
yNow = t.clientY;
|
|
const now = performance.now();
|
|
const dtv = (now - evT) / 1000;
|
|
if (dtv > 0.002) {
|
|
const inst = (evY - yNow) / dtv; // finger up → positive (offset grows)
|
|
vel = 0.8 * vel + 0.2 * inst;
|
|
evY = yNow;
|
|
evT = now;
|
|
}
|
|
e.preventDefault();
|
|
}, { passive: false });
|
|
|
|
window.addEventListener("touchend", (e) => {
|
|
dragging = false;
|
|
lastT = performance.now();
|
|
e.preventDefault();
|
|
}, { passive: false });
|
|
|
|
window.addEventListener("touchcancel", () => { dragging = false; vel = 0; }, { passive: false });
|
|
|
|
// ---- lifecycle ------------------------------------------------------------
|
|
window.addEventListener("load", () => {
|
|
paint();
|
|
applyOffset();
|
|
requestAnimationFrame(frame);
|
|
try {
|
|
window.__TAURI__.event.emit("tttest-log", {
|
|
msg: "webview up " + window.innerWidth + "x" + window.innerHeight +
|
|
" dpr=" + window.devicePixelRatio,
|
|
});
|
|
} catch (e) { /* desktop */ }
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|