The release flow, install policy, and on-device rules previously lived in
session context. Now:
- scripts/release.sh: the executable release — gate 1 static checks,
gate 2 full go test (incl. TestNoFramesWhileIdle), gate 3 frame-regression
profile on the emulator, build (SKIP_CHECK=1 avoids double static checks),
then install to EVERY connected device (emulators and phones; the
wireless-debugging serial changes per reconnect, the loop sidesteps it).
Any gate failure aborts before install; dirty tree is warned, not fatal.
- doc/release.md: the process, the install-to-all-devices default, the rule
that a phone is INSTALL-ONLY during a release (on-device
profiling/testing is diagnostic and needs explicit approval per run),
failure-handling table, versioning gap (still gogio default 1.0.0.1), and
emulator requirements.
- doc/README.md: release.md added to the doc table.
- architecture.md §11: pointer to the release flow.
Validated end-to-end: release.sh ran all three gates (PASS), built the APK,
and installed it on both the phone and the emulator in one command.
Density (pure hi-DPI) was already scale-free: all bookkeeping is in
density-dp and the scale enters only at the px<->dp boundary. But Android
also has a second axis, the user font-size setting (PxPerSp = fontScale *
PxPerDp), and the shaper draws baselines in sp. At a non-default font
scale the rendered line pitch is 16.8*fontScale dp while every logic-side
consumer used the raw 16.8 dp: taps would misplace by up to (fontScale-1)
viewportfuls of lines and scroll clamping would stop short of the bottom.
- ScaleEvent.FontScale + Frame.FontScale closed loop (main reads
gtx.Metric, logic tracks it in State.fontScale).
- EffectiveLineHeight()/EffectiveLineHeightAt(): the font-scale-applied
line height, now used by every consumer (window start, sub-line
remainder, tap mapping, scroll clamp, page size, cursor vertical move,
menu position, chunk-prefetch fallbacks).
- Renderer: GlyphLayout.LineHeight, caret, selection handles, and
highlight all use the scaled ascent/line-height from gtx.Metric.
- font_scale_test.go: 2000-pair tap property test at fontScale 1.3 with
the glyph layout fabricated at the scaled pitch (independent ground
truth), plus EffectiveLineHeight unit test.
- On-device: tap markers landed on exactly the tapped line at font_scale
1.3 (fsline060/080/081) and 0.8 (fsline039); rendered pitch measured
57/35/44 px at 1.3/0.8/1.0 (matches 16.8*fs*2.625); settled-position
window start k = floor(s/lh_eff) verified against the visible top line.
- Docs: architecture.md 6.2 font-scale axis, README two-scale note +
profiler 2s flush staleness note, development plan v10 Phase 11.
The screen->line tap mapping only needs the sub-line scroll remainder
(tapLocalY adds r, never the full scroll) because the visible glyph
layout is window-relative and IMEWindowStartByte re-anchors it to the
file. That holds for every scroll offset IF the window start line
k=floor(s/lh) and the sub-line remainder r stay consistent.
Property test (4000 random scroll/tap pairs, asserting against an
independent drawn-geometry ground truth, not the tap code's own math)
exposed a real bug: int(s/lh) in the Dp float32 domain rounds the
quotient to nearest and can round UP across an integer boundary while
the float64 mod still reflects the line below. In a sub-pixel-wide
band of scroll offsets the window started one line too far while the
draw shift lagged by one line - the whole rendered window (and every
tapped line) shifted by one.
Fix: one shared float64 floor decomposition (scrollDecompose) used by
the window start (visibleByteRangePrecise/Estimate), the renderer's
sub-line shift (visibleScrollOffset), the tap mapping (tapLocalY), and
chunk prefetching. Also route the shaper's line height (previously
ignored by visibleByteRangePrecise) through VisibleByteRange.
On-device cross-check: at s=4246.9 (r=13.3) and s=4210.7 (r=10.7),
taps on visually identified lines typed markers that landed on exactly
those lines in the file on disk; profiler ScrollDP, screenshot,
formula, and disk all agreed.
Docs: scroll decomposition invariant in architecture.md §6.2, pipeline
hop 2 in doc/README.md, Phase 10 in development_plan.md.
doc/README.md §Screen coordinates: add the complete 4-hop pipeline
(screen px -> app dp -> text-local dp -> window-relative glyph byte ->
absolute file byte), the tapLocalY sub-line-remainder rule, the
glyphBase window-base rule, and the selection/menu/IME coordinate
spaces (absolute file bytes vs window-relative TextField selection vs
app-dp drag events vs menu-local MenuItem offsets). Cross-reference
the architecture.md invariants.
doc/architecture.md §8: state that CheckGestures input-event positions
are app-local Dp (same space as element Regions / MenuRect) and that
logic converts them to text coordinates.
The coordinate-space confusion (screen px 1080x2400 for input tap/
screencap, display px 900x2000 for reading PNGs, app-local px offset by
the 128px status bar, pt at density 2.625) cost real debugging time.
Wrote it down in doc/README.md as the permanent reference:
- the four spaces + conversions (display x1.2 = screen; screen Y-128 =
app-local; /2.625 = pt)
- the practical tap rule: read (x,y) off the PNG, x1.2, input tap --
screenshot px ARE screen px, no 128 offset for tap-what-you-see
- fixed geometry (status/nav bars, Gboard rows, editor region in pt),
with re-measure guidance
- tap-test pitfalls (first-tap-swallowed, first input text can fail,
wrong-place text = cursor/selection suspect not tap math, don't
memorize drifting content positions)
All keyboard row values re-measured from a fresh screenshot and verified
live: tapping the Q key at (62, 1712) and backspace at (990, 2020) both
landed first try (file content confirmed). This also exposed that the
old notes had mixed display-px row values with screen-px key values in
the same list -- exactly the confusion this section eliminates.
Also fixed the stale Phase 6 'letterboxed window' note in
development_plan.md (window is full-screen; only the 128px status-bar
offset is real; content positions drift; the OpenFileChan logcat line
was removed) and pointed emu.sh's tap help at the x1.2 rule.
Replaces the ad-hoc adb/nohup sequences used throughout verification with
one self-contained script: up/down/status, app start|stop|restart, shot,
log, tap, type, cmd (one-shot editor debug command), perf on|off|pull,
push/pull of Notes test files. Encodes the machine-local paths
(Notes/, PadPerf/ profiler + cmd file) in one auditable place.
Reliability findings baked in (each hit during testing):
- The AVD auto-saves a 'default_boot' instant-boot snapshot on clean
shutdown; a corrupted one makes the emulator segfault during restore
(device never comes online). up() detects it (process death / no adb
device within PAD_ONLINE_TIMEOUT) and falls back to a cold boot with
-no-snapshot-load; the next clean down() re-saves a good snapshot, so
this self-heals. down() is the only sanctioned stop (clean kill saves
the snapshot; SIGKILL corrupts it).
- The emulator process identity is ambiguous (launcher vs
qemu-system-*-headless), and crashpad/netsimd children carry the AVD
name in their command lines. Liveness and kill therefore use a tight
signature: comm matches qemu-system-* AND cmdline contains ' -avd
<AVD>' (never a broad pkill -f pattern — one matched and killed the
calling shell during testing).
- Before any launch, orphaned qemu instances are cleared (they hold the
AVD lock and make new launches fail silently); before starting a
replacement, the old one must be fully gone (lingering device/lock
causes false success).
- Timeouts: short online window for the snapshot attempt, full
BOOT_TIMEOUT for the fallback cold boot (~20 s).
Tested: idempotent up, 10 s snapshot-restore boot, forced fallback
(30 s end-to-end, verified correct instance), orphan cleanup, perf/cmd/
push/pull/shot/log on-device. doc/README.md now documents it.
The Android build recipe lived at /tmp/build_pad.sh and depended on
/tmp/apktool.jar; /tmp gets wiped between sessions (both were lost once
mid-project and had to be rebuilt). Move the recipe into the repo as
scripts/build_emu.sh:
- checks prerequisites (java, gogio, apksigner, adb, debug keystore) with
actionable error messages
- auto-downloads apktool v3.0.3 to the stable ~/android-sdk/tools/ if
missing (never /tmp)
- uses a mktemp work dir cleaned via trap; no /tmp clutter
- --no-install flag; verifies an adb device is present before installing
- verifies the permission injection actually took effect
Tested end-to-end (build + install + app relaunch) and idempotent on
re-run. doc/README.md and development_plan.md now point here.
internal/perf: single-goroutine profiler (no locks). When enabled by the
/storage/emulated/0/PadPerf/enable marker it records one CSV row per logic
frame (seq, ms, frame delta, page, scroll Dp, max-scroll Dp, total lines,
visible byte range), logs a rolling ~1/s summary, and on Stop reports
nearest-rank p50/p90/p99/max. Flushes per row-batch but never fsyncs per
flush (avoids periodic hitches in the logic path).
editor: PerfRecord package-level hook (nil when off) + ProbeRecord;
Logic.emitFrame() now centralizes every frame emission so the profiler sees
each logic frame exactly once, on the owner goroutine. State gains
VisibleStart/VisibleEnd so the probe can confirm shaping stays
viewport-bounded.
logic: optional debug cmd poller (off by default) watches <dir>/cmd as a
one-shot file (top/bottom/frac <0..1>/dp <int>) and the owner applies a
clamped [0,MaxScroll] jump + frame. Enables deterministic large-offset scroll
tests without pixel taps.
main: wires the profiler when the marker file exists; logs present-fps
every 2s; stops the profiler on Destroy.
docs: README package inventory (add internal/perf); architecture §11
profiler facility; spec §5 invariant 7 (scroll always clamped to
[0,maxScroll]) + §6 measured rows; development_plan v5 + Phase 6 results.
Verified: go build/vet + full -race green. On-device 10 MB file:
logic-frame cadence flat across offsets 0.02->1.0 (no large-offset
degradation), visible byte range <=4.3 KB at every offset, clamping exact
across 2->130,955-line files, PSS plateaus ~250 MB (bounded, no leak);
profiler overhead negligible.