Commit Graph

25 Commits

Author SHA1 Message Date
347b6825cc gtauri viewer: swap element roles on commit — no resize on takeover
A second drag while the first was settling showed the new image briefly
at the wrong size: the commit copied the preview's src into vimg (an
async paint — one frame of old content in the new box) and openViewer
then reset vScale=1, so the promoted image rendered at thumb scale
until full-res landed.

Commit now swaps the ELEMENTS' roles instead of copying pixels: the
preview element already shows the incoming image at its own size and
scale, so finishCommit promotes it in place (geometry adopted from
the loaded preview, moved to 0, brought to front, old primary hidden
immediately). No src copy, no async paint, no re-layout — the settled
frame is the drag frame. P/S role variables replace the fixed
vimg/vimg2 references throughout the viewer.

openViewer(keepVisible) no longer resets the view state (the commit
already set it), and a full-res landing while a slide owns
base/vScale is deferred to the safety timer instead of jumping the
animation.

Verified on the emulator: mid second-drag the promoted image holds
its exact fit size (1081 px) in every captured frame (the bug showed
~288), both images visible on the strip, and the chained landing is
pixel-identical to two deliberate swipes.
2026-11-03 14:32:24 -05:00
695e2417cc gtauri viewer: mid-commit touch/key takes over the swipe (chained swipes)
A second swipe while the new image was still sliding in got dropped
(pointerdown bailed while the commit tween was in flight). Now a
touch — or arrow key — during a commit takes it over: the commit
finishes instantly (roles swap at the settle positions, same
dimension-adoption path as natural completion) and the input starts a
fresh gesture on the landed image, so rapid successive swipes chain
through multiple pictures. A plain tap during a commit simply settles
it early.

Verified on the emulator: two 100 ms flicks 120 ms apart (the second
lands mid-commit) end on the exact same image — pixel-identical — as
two deliberate slow swipes.
2026-11-03 13:47:39 -05:00
cd31b76889 gtauri viewer: velocity-scaled swipe commit (faster flicks)
Flicks now continue the finger's motion instead of taking a fixed
200 ms: commit duration = remaining distance / release velocity
(15 % snappier), clamped 80-240 ms, ease-out. A full-speed flick
releasing past half the screen commits in ~80 ms (was 200). Slow
releases past the 25 % threshold still settle gently (180 ms
smoothstep; 150 ms for keyboard nav).

Verified on the emulator: a 100 ms flick is fully settled before
the first post-release screencap (~150 ms), with only a 5 px
edge artifact between consecutive frames (no residual motion).
2026-11-03 13:28:43 -05:00
ba646fb233 gtauri viewer: adopt incoming dimensions at swipe commit (aspect fix)
Settling on a different-aspect neighbor (e.g. landscape → portrait)
stretched: vimg kept the OLD image's width/height styles while showing
the new thumbnail, so the new pixels rendered in the old box until
full-res landed and fitViewer() reset the dimensions — a visible
vertical stretch then snap-back.

At the commit swap, adopt the incoming image's dimensions from the
drag preview (nav already knows w/h/scale from its loaded thumb):
base/vScale/width/height/transform are all set consistently in the
same task as the src swap, so the first settled frame is correct.
Fallback: if the preview never loaded (keyboard nav / slow thumb),
use the plain openViewer path where the thumb round-trip sets the
dimensions before first paint.

Verified on the emulator with a 4:3 landscape + 3:4 portrait pair:
settled content box 1439x1921 (0.749 = exact 3:4) from the first
settled frame, unchanged after the full-res swap.
2026-11-03 13:15:55 -05:00
029780cc09 gtauri viewer: two-image swipe commit — outgoing stays until off-screen
Fix: on release the old image vanished instantly and only the new one
slid in. Now both images live on the page strip for the whole commit:
vimg (outgoing) slides off-screen while vimg2 (incoming, the drag
preview) slides home, over a 200 ms smoothstep; only then do the
elements swap roles (vimg.src = the already-decoded preview image,
vimg2 hidden after the swap paints — no blank frame).

Details:
- navigate() returns whether it committed; at the library edges it
  refuses and the drag snaps back (previously a past-the-edge drag
  could leave the image stuck mid-strip).
- commit tweens are locked: pointer/dblclick input is ignored for the
  ~200 ms so a second gesture can't fight the animation.
- openViewer gains a keepVisible (promoted) path that skips the
  hide+thumb round-trip; vFullSeq reset per open (safety-net counter
  could previously exhaust after ~11 opens).
- removed the now-dead slideFrom slide-in mechanism (its purpose is
  subsumed by the two-image commit).

Verified on the emulator: mid-commit frame shows both images
(outgoing sliding off left, incoming from right); snap-back slides
both back; edge drag right on the first image snaps home.
2026-11-03 13:04:53 -05:00
16cd868f22 gtauri: pure-push watching only; one emit per burst; live swipe preview
Watch de-duplication (per the plan review): FileObserver (inotify,
push) and MediaStore ContentObserver (URI push) are the ONLY change
detectors. Removed: the 100 ms recv_timeout tick, the fast_dir_sig
mtime-diff watcher, collect_watched_dirs, changed_dirs, dir_sig,
rescan_subtrees, and the 5-minute full-rescan timer thread.
watch_files now blocks on the push channel; on Android each wake
coalesces ~50 ms so a burst of N file events becomes one payload
batch, one SQLite connection, and ONE grid emit (verified on the
emulator: 20-file copy = 1-2 emits, 20-file delete = 1 emit,
single add still lands in ~75-95 ms).

FileObserver rewritten: recursive registration (Pictures/DCIM tree,
cap 5000 dirs) and self-extending — a CREATE/MOVED_TO on a directory
spawns a new observer for it, so new albums are watched too.
tif/tiff added to the image filter.

Dead code removed (the Tauri port never wired them): scan::walk,
walk_dir_nostats, walk_dir_into, mtime_ns, decode_jpeg (full-size),
images::thumb_png, db::set_thumb, db::list_entries, db::count,
db::delete_by_media_id; Thumb shrunk to {rgb,width,height} (the
grid is uniform squares; the viewer uses full-res). Tests now
compile (tempfile dev-dependency was missing — the test module
never built) and were fixed to match the current API.

Viewer: live swipe preview — while a horizontal drag is in flight at
fit scale, the picture being swiped in renders on the page strip
(second <img>, its grid thumb, a ~1 ms mem hit) and tracks the
finger, so there is no blank side. On commit the primary image takes
over from exactly where the preview was; on snap-back the preview
slides back with the tween. Verified on the emulator (two images
visible mid-drag, clean slide home).

Not re-verified: 120 Hz jank (per instruction — measure once when
settled). Desktop paths untouched (deferred).
2026-11-03 12:49:30 -05:00
bca6a9de4a gtauri viewer: horizontal swipe navigation (left/right to next/previous)
Standard gallery swipe: at fit scale a horizontal drag follows the
finger 1:1 (rubber-banded past the page edge); on release, past 25% of
the width or a fast flick (>0.35 px/ms), the neighbor in the list
opens with a 150 ms ease-out slide-in from the drag direction.
Short drags snap back; vertical drags at fit are inert; zoomed drags
still pan and pinch still zooms. Arrow keys navigate on desktop.

Verified on the emulator: left swipes advance, right swipe returns to
the previous image pixel-identical, 80px drag and vertical drag do not
navigate.
2026-11-03 11:23:41 -05:00
314022cfb2 gtauri: implement 100% pure-push FileObserver in Kotlin (sub-50ms instant add/delete) 2026-11-03 11:02:40 -05:00
471d2faeab gtauri: zero-delay handling for Android MediaStore content watcher events (immediate push) 2026-11-03 09:09:34 -05:00
15a857ed7f gtauri: enable fast mtime-diff filesystem watcher on Android (adds & deletes reflected in ~1s) 2026-11-03 08:49:06 -05:00
10ec0b52cc gtauri: fix phantom rows, dead deletion detection, 5-min launch stall
Root causes found by reproducing each on the emulator (2.8k-file fixture
with mixed jpg/mp4):

1. walk_dir_nostats (incremental rescan of >2000-file dirs) had NO
   image-extension filter: it re-enumerated every file, injecting all
   ~1,727 videos into the DB. This is what made the count swing
   8,439 -> 10,166 and the grid full of blank cells (404 rows).
   Now filters through is_image_path() like every other walk.

2. walk_dir aborted the ENTIRE walk on any per-file stat failure
   (file deleted mid-walk), so an error during the walk meant the
   walk was never applied and deletions were never detected.
   Per-file stat errors are now skipped; only readdir/root errors
   fail the walk (an errored walk is still not applied).

3. dir_sig stat'ed every entry of every root dir every watch tick:
   10k+ stats/second on the phone = 10s+ of FUSE I/O per second =
   the long delays. On Android the MediaStore URI payloads already
   carry the exact change signal, so the mtime-diff machinery now
   runs desktop-only. The Android watch tick is a single read_dir
   of our own app dir (no-op when empty): 1s backstop, 30s-throttled
   fallback full rescan for events with no payload, 5-min full pass.

4. TOCTOU: a delete consumed while a full rescan was mid-walk lost
   to the walk's stale snapshot (walk re-added the row). RESCANNING
   gate parks payloads during the walk; they apply after it, so
   mid-walk deletions always win. Reproduced deterministically
   (churn during the startup walk), fixed, re-verified.

Emulator stress (all passing): full-walk convergence, add +1s,
delete +1s, video add/delete = zero rows, 60-op churn converges to
exact disk count, 3x relaunch idempotency, startup-walk race
converges (1s normally, 5-min backstop when the OS scanner drops
burst events), no-blank-cells visual check, 0.00% dropped frames.

Phone verified: 10,166 -> 8,439 on first launch (all video phantoms
purged), add appears ~2s after the OS indexes it, delete same,
DB returns to 8,439 exactly. ContentObserver registration + onChange
logging added (it fired reliably once the stale instance was
replaced).
2026-11-02 12:37:42 -05:00
bb349dac82 gtauri: URI-driven MediaStore events; remove truncation machinery
Watching is now exactly what the OS tells us, with no filesystem
scanning on the event path:

- MainActivity's ContentObserver receives the changed URI from
  Android. It queries that ONE row (~ms, no debounce) and drops a
  one-line payload: A|media_id|path|size (row exists) or
  D|media_id (row gone). Per-event files (.ms_recent.N, never
  overwritten) so a burst cannot clobber an unread event. A zero-arg
  JNI call wakes Rust; a 1 s backstop also drains the queue, so a
  lost wake self-heals in a second.
- Rust applies the payload directly: upsert with media_id stored, or
  delete resolved by id (items row first, then a media_map table that
  records id→path for EVERY add event — including rows created by a
  filesystem walk, which doesn't know the id). Content changes reset
  decode-failure state so a still-being-written photo retries.
- The disk is the truth: the payload size is ignored in favor of a
  stat (the scanner can leave _size NULL).
- The full FS walk remains as startup baseline + 5-min reconciliation,
  and the mtime-diff incremental rescan remains as a second
  independent path. Verified on the emulator: add → grid in ~2 s
  (incl. OS scanner), delete → grid in ~2 s via media_id, missed wake
  drained in 1 s.

Truncation machinery REMOVED (per user: FUSE is reliable; the
'truncations' were counting errors and a stale-DB cross-check firing
on legitimate changes): the two-strike mass-delete guard, the
walked_max watermark, the layer-2 re-walk, the per-directory stale
cross-check, and the read_dir_full retries are all gone. The one
invariant that replaces them: a walk that hits ANY filesystem error
fails and is NOT applied (an error is not 'those files are gone').
The phone DB now converges to the exact disk count (8,439 = 8,399
Camera + 27 Screenshots + 11 Messaging + 2 Bluesky); the earlier
10,166/8,438 swings were real disk-state changes, not truncation.

Emulator display fixed: the AVD image ships with
debug.hwui.drawing_enabled=0 (every window renders black, HWUI
drawing disabled system-wide). 'adb shell setprop
debug.hwui.drawing_enabled 1' after each boot restores rendering —
it is a runtime prop, so it must be re-set on every cold boot.
2026-11-02 11:09:29 -05:00
1bc1d6beb3 gtauri: fix camera-photo latency, delete blank boxes, truncation guard
- Camera-photo latency: root cause was READ_MEDIA_IMAGES never
  granted (granted=false on the phone), so the MediaStore fast-path
  query silently returned nothing and photos only appeared via the
  slow full FUSE walk. The app now requests the permission at
  runtime (once, with a single retry on denial). The camera writes
  through MediaStore at capture time, so the ContentObserver fires
  immediately and the photo appears in ~1 s.
- Delete blank boxes: two JS flaws — (1) no img onerror handler, so a
  cell that requested a thumb for an already-deleted file (404) sat
  as an empty dark box forever; (2) recycled cells were matched by
  slot index only, so after a deletion shifted the list a cell could
  keep a stale id. Now: onerror drops stale ids (idSet membership) or
  retries transient ones, and cells reassign on idx OR id change.
- Truncation guard (chicken-and-egg): the mass-delete suspicion
  threshold was 20% but observed FUSE truncations lose 15-17%, which
  sailed under it and poisoned the DB. Now 10%; a confirmed deletion
  (two-strike passed) lowers the new walked_max watermark, a refused
  one keeps it. Layer-2 re-walk compares against max(db, walked_max)
  so a poisoned (shrunk) DB cannot silence truncation detection.
  Emulator suite: healthy→truncated (all 3 layers, 0 loss),
  poisoned-DB→truncated (layer 2 now fires), healthy walk self-heals.
- Verified on the phone: DB matches disk exactly (8,398 Camera +
  27 screenshots + 11 messaging + 2 bluesky = 8,438); an earlier
  10,166 count was a transient user-system state, not truncation.
2026-11-02 08:01:13 -05:00
9e52dc0355 gtauri: MediaStore fast path + js-ready handshake + virtualized grid
- MediaStore fast path: Kotlin's ContentObserver queries MediaStore
  for images added in the last 120s and writes path|added|size lines
  to .ms_recent, then wakes Rust via the proven zero-arg
  nativeFsChange() JNI. Rust upserts those rows and emits
  immediately — a camera photo appears in ~1s without waiting for
  the ~10s FUSE re-read of a 10,000-file directory. (Direct
  Kotlin->Rust JNI string passing and Rust->Java calls crash under
  the emulator's x86_64->arm64 native bridge; file handoff plus
  zero-arg JNI is the only reliable channel.)
- js-ready handshake: the first gallery-files emit fires ~30 ms
  after process start, before the WebView's JS exists (Tauri events
  are fire-and-forget, so it was lost). JS now signals
  'gtauri-js-ready'; Rust re-emits the current state.
- Virtualized grid: the document is ONE spacer (full scroll height)
  plus ONE band of recycled cells pinned by transform — ~80 DOM
  nodes total for a 10,000-photo library. Launch cost no longer
  scales with library size; the first screen renders as soon as
  the WebView boots and the first thumbs arrive.
- 'Missing photos' report resolved: the full walk of the 9,961-file
  Camera dir enumerates exactly 8,438 images with no guard firing;
  the difference is videos, which a photo gallery correctly
  excludes.
2026-11-02 00:02:48 -05:00
58b203e4fa gtauri: event-driven watching + defense against stale VFS dir listings
Live watching is now event-driven, no tree polling:
- Android: MainActivity registers a MediaStore ContentObserver
  (Images, notifyForDescendants) and wakes the Rust watcher thread via
  a JNI call (Java_wow_gmp_gallery_MainActivity_nativeFsChange) — a
  single channel send, no main-thread work. Covers camera saves,
  media-scanner indexing, other apps' indexed writes.
- Desktop: inotify/FSEvents via notify on the scan roots.
- On wake: coalesce + debounce, diff dir mtimes, rescan only the
  changed subtrees. The 1 s recv timeout doubles as a cheap backstop
  (raw out-of-band copies the scanner hasn't indexed yet); a 5-minute
  full pass is the safety net. minSdk 30 (3-arg ContentObserver API).

Stale-listing truncation bug found and fixed (reproduced on the
emulator's 9p /sdcard; the phone's FUSE does the same): a readdir over
a large directory can return a SILENT early EOF — a stale cached
listing with NO error. The phone's 9961-file Camera dir truncated at
~8400 once and db::apply deleted ~1700 good rows. Three layers now:
1. scan::read_dir_full — retry whole-dir reads on mid-stream errors;
2. per-directory cross-check in walk_dir: if the enumeration is far
   below the DB's count for that dir, re-read up to 3x keeping the
   best listing; whole-walk re-check in rescan keeps the larger of two
   walks;
3. db::apply mass-delete guard: never delete >20% of the DB in one
   apply unless a previous walk already saw the low count (two-strike;
   a meta.walked row tracks it). Genuine gradual deletions still flow
   through (verified: a 4000-file mass move streams the grid down
   progressively), and a second consecutive low walk still deletes.

Emulator reliability suite (all pass): 3x launch count stability,
add +1 / remove -1 in ~2 s, 20-file burst, DB recovery from damage,
stale-listing refusal (0 rows lost), two-strike delete, live restore,
idle 0.0% dropped frames, deep fling loads thumbnails. A/B vs ttimg
(no-backend CSS-scroll control): adb-swipe drop deltas are at the
input-injection artifact level, idle identical (0.0%).
2026-11-01 20:24:01 -05:00
bd27a7ccc1 gtauri: fix live watching (incremental subtree rescans)
Root cause of 'adds/removes not showing up': the watcher's file-COUNT
tree signature took 8-10 s per tick on the phone (FUSE readdir of the
8400-file Camera dir is ~1 ms/entry), so the 1 s poll was effectively
a 10 s walk, and the old 30 s forced FULL rescan (walk+stat of every
file, ~30 s) kept the watcher thread perpetually busy.

New design:
- 1 s tick stats DIRECTORY mtimes only (~1 ms). Verified on the phone:
  Android's FUSE DOES update dir mtime on file add/remove (stat
  before/after a copy showed it) — the earlier 'never fires' reading
  was wrong; no files had actually been added in that trace window.
- On a changed dir: incremental rescan of ONLY that subtree. Small
  dirs (<=2000 files) get a full walk with stat (adds, deletes AND
  in-place edits). The huge Camera dir gets a readdir-only walk: new
  files are stat'd, known files keep their DB mtime/size, deletions
  are detected by set difference. Everything outside the changed
  subtree keeps its DB metadata — cost is proportional to the change.
- Full rescan every 5 min as the safety net (in-place edits in the
  huge dir, anything missed).
- DB: busy_timeout(10s) so the startup scan and watcher can transact
  concurrently (WAL).

Measured on the phone: add to Pictures -> grid +1 in ~5 s; add to
Camera -> +1 in ~12 s; removals picked up on the next tick. Also
observed tracking a live 1700-photo user import (grid 8440 -> 10166
as the import progressed).

JS: event payload normalizer for thumb-ready/full-ready (object vs
JSON string across WebView builds), and js files/emit instrumentation
to compare send vs receive counts.
2026-11-01 18:55:31 -05:00
312d87f15d gtauri: pinch zoom about pointer midpoint + live filesystem watching
Pinch: the image now follows the hand (midpoint drift) and zooms
ABOUT the midpoint — the image point under the fingers stays put
while the scale changes. Per-frame delta math, clamped as before
(fit min, image-edge pan limits).

Live watching: new photos appear in the grid within ~2s of being
written. inotify does not work on Android's FUSE-backed external
storage, so the watcher polls a tree signature (dir mtimes only —
a few dozen stats/s) every second; on change (or a 30s safety-net
full pass) it debounces 600ms for the write burst, then runs the
incremental rescan (walk + DB diff + emit) off the main thread.

Supporting changes:
- Staged corruption: a decode failure no longer marks an id broken
  immediately — 3 consecutive failures are required, and a success
  clears the count. A photo still being written fails once or twice
  and then decodes; genuine garbage gives up and stays gray.
- Grid rebuild preserves the viewport across a live rescan (pins to
  the cell that was on top), so a new photo landing at the top does
  not jump the scroll position.
2026-11-01 18:02:09 -05:00
8f700e7f66 gtauri: clamp viewer pan/zoom to image edges
Pan is now limited to the image bounds (|offset| <= (rendered -
viewport)/2 per axis), so the image can never be dragged off-screen
leaving blank space around it; on a letterboxed axis panning is
locked to centered. Pinch can no longer zoom out past fit. Clamping
lives in vApply() so pan, pinch and reset all share it; still
compositor-only (transform only).
2026-11-01 17:44:14 -05:00
d990627200 gtauri: distance-from-viewport drop policy for ALL jobs
The backlog must never delay on-screen work. Urgent jobs (real
Chromium requests) are now stale-dropped too, not just prefetch:
a fast fling queues urgent work for every band it crosses, and all
of it except the final band is dead — it filled the queue cap and
made fresh on-screen requests wait for the JS retry timer.

Safe because the anchor now settles at the final scroll position
(trailing send), and on-screen cells have idx >= VPORT so they can
never be dropped. A pure LIFO would not fix this: stale jobs sit
at the stack bottom, never popped, still blocking the cap.

Emulator fling test: 309 stale urgent jobs purged, queue peak 28,
all 850 decodable images loaded.
2026-11-01 15:38:22 -05:00
02d1f097ac gtauri: fix deep-scroll stall + instant viewer
Root causes found via per-event tracing (trace.log) + heartbeats:

1. Decode backlog: single worker at ~300ms/photo could not keep up with
   fast long scrolls — phone session ended with a 2400-deep queue
   draining at ~3 cells/s (13+ min to catch up). That was the
   'scroll far down and nothing loads no matter how long I wait' zone.
   - 4 parallel decode workers (~12 photos/s)
   - viewport-aware stale drops: prefetch jobs more than 60 rows behind
     the viewport are dropped, not decoded (fleeing leaves no backlog)
   - Q_CAP bounds the queue; JS 1.5s safety net re-requests
   - queue peak in repro test: 42 (was 2405); all 850 decodable
     emulator images now load (708 before)

2. Stale-anchor bug: sendAnchor was leading-edge throttled with no
   trailing call — if a scroll ended inside the 150ms window the Rust
   anchor stayed stale forever, and the worker then dropped fresh
   URGENT jobs (tap → full read) as 'off-screen'. Fix: trailing
   anchor send; and URGENT jobs are never stale-dropped (only
   prefetch work is droppable).

3. Viewer latency: full-res read ran on Chromium's main thread
   (wry shouldInterceptRequest is synchronous) — 30-50MB flash read
   froze the whole WebView. Now: thumb shown instantly (mem cache,
   ~1ms), full read queued urgent and done off-main; full-ready event
   swaps in the full-res (256MB full-res LRU makes re-opens instant).
   Measured click→full-res: 4ms on emulator.

4. Scan: skip OS junk (._* AppleDouble forks, dotfiles) — the phone's
   DCIM carried 72 of them, rendering as permanent gray cells.

5. Viewer for a corrupt file now shows 'image unavailable' instead of
   'loading…' forever; grid retry timers skip far-off-screen cells.
2026-11-01 09:11:11 -05:00
gmp
e019a950b1 gtauri: Tauri v2 gallery harness WORKING on emulator
Chromium owns scroll/compositor/lazy-load/image-cache; Rust owns
scan/SQLite/thumbnail-decode behind a gallery:// custom protocol.

Hard-won gotchas (see in-code comments):
- Android: no native custom-scheme support → wry rewrites the URL to
  http://gallery.localhost/... (custom_protocol_workaround). The JS
  MUST request that form; the handler receives the reverted
  gallery://localhost/... and must strip the authority.
- tauri_build::build() in build.rs must run UNCONDITIONALLY (build
  scripts run on the host: a cfg(target_os) gate silently disables it,
  which skips the rustc-cfg=mobile emission → no mobile_entry_point →
  .so with no JNI symbols → UnsatisfiedLinkError).
- build.rs must list frontend files for rerun-if-changed (a dir-level
  directive only watches the dir mtime, not content edits) — the .so
  bakes the frontend at compile time and cargo otherwise ships stale
  UI with a 'successful' build.
- logcat drops ~75% of lines under load: use the file trace.
- fetch() is CORS-restricted (page origin http://tauri.localhost);
  <img> is not — add ACAO:* anyway.

Emulator (5853 real files): grid renders, lazy thumbs via
gallery:// with on-disk cache (87 files 35-72KB), FR log clean at
60Hz, viewer opens full-res with compositor-only pan/zoom. The single
'broken' cell is a 17-byte fake test JPEG (correct behavior).
2026-10-31 18:59:51 -04:00
d82ecf74ac harness: tttest — Tauri 2 / WebView scroll-jank testbed (third stack)
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.
2026-10-31 13:53:43 -04:00
73f27262f7 harness: sync egtest spin-driver build 2026-10-31 06:18:42 -04:00
47c5c972fe egtest: spin-driver frame pacing + nice -10; fr_capture.py tool; 0.12% drops at 120Hz (vs sltest 0.33-0.83%) 2026-10-31 06:18:20 -04:00
2cfb32c746 harness: egtest — egui 8580-row grid (egui port control experiment) 2026-10-31 00:22:02 -04:00