Skip to content

feat(input): touch hit facts — bounds hits resolved by the host, carried per contact - #209

Merged
doodlewind merged 1 commit into
mainfrom
feat/touch-hit-facts
Aug 3, 2026
Merged

feat(input): touch hit facts — bounds hits resolved by the host, carried per contact#209
doodlewind merged 1 commit into
mainfrom
feat/touch-hit-facts

Conversation

@doodlewind

Copy link
Copy Markdown
Collaborator

Phase 2 of the touch program, change A (design context: the gesture layer review that closed #177).

What changes

The guest no longer asks where a finger landed — the host tells it.

before after
hit resolution guest queries ink hitTest (op 27) on the down edge host resolves a bounds hit once per NEW contact, against the committed frame, and carries it for the contact's lifetime (frame() arg 4, parallel to touches)
semantics ink-claiming: row gaps missed → VirtualList.touchRect geometry workaround layout-box claiming (op 42 hitTestBounds): a finger in a row gap still owns the list; touchRect deleted
overlay layers ink transparency was implicit explicit hitPass prop (32): the layer's own box never claims, its portal content still does
  • core: draw::hit_test bounds mode (shares the whole walk), Ui::touch_hits 8-slot capture table + tests
  • hosts: vita frame_with_input, wasm ui_hit_test_bounds, sim/golden-oracle TS table twin
  • framework: TouchContact.hit/GestureContact.hit, resolveTouchHit fallback chain (fact → op 42 → op 27 → rects), devtools replay re-resolves deterministically

The bug the design caught

First full run: every OSK tap journey went dead — the framework's full-screen overlay layer, a pure layout box, swallowed every bounds hit. That's the honest cost of bounds semantics, and hitPass is its principled fix (pointer-events:none, self-only, typed and local). The im-sim OSK journey now pins it forever.

Validation

core 110 · contract byte-equal · gesture+touch 23 · virtual-list 16 · im-sim 11 · canonical chain · 52 web goldens byte-exact · Vita3K e2e 52/52 byte-exact — the fact channel changes zero pinned behavior; it removes the query and the workaround.

🤖 Generated with Claude Code

…ied per contact

The gesture layer used to ASK where a finger landed (an ink hitTest query on
the down edge) and the answer had holes: ink misses on row gaps forced every
list to hand the framework its own screen geometry (VirtualList.touchRect —
an app feeding the runtime numbers the runtime already owns). Hits are now
FACTS the host delivers, and they resolve by layout box, not ink:

- spec: op 42 hitTestBounds (the bounds-only hit twin — pure layout
  containers claim their box, UIKit semantics) and prop 32 hitPass (a node's
  own box never claims, descendants still tested — pointer-events:none,
  self-only). Frame contract: argument 4 `hits`, parallel to `touches`.
- core: draw::hit_test grows a bounds mode sharing the entire walk (paint
  order, clips, transforms, opacity culling); Ui::touch_hits keeps the
  8-slot contact-id -> hit-at-down capture table — a NEW contact is resolved
  ONCE against the committed frame the user was looking at and carried until
  it lifts. The framework marks its overlay/portal layer hitPass so bounds
  facts resolve THROUGH empty overlay space to app content (found the hard
  way: the OSK journey went dead when the full-screen layer swallowed every
  contact).
- hosts: vita frame_with_input resolves via the core table and passes arg 4;
  wasm exports ui_hit_test_bounds; the sim and the golden oracle carry a TS
  twin of the table (framework/src/touch.ts createTouchHitFacts).
- framework: TouchContact/GestureContact grow `hit` (the down fact);
  resolveTouchHit (input.ts) is the one touch-hit authority — fact first,
  then op 42, then op 27 (stale hosts), then region rects. VirtualList drops
  touchRect entirely: rows resolve by node identity, and a row-gap tap is a
  separator tap (it doesn't press). Devtools replay drops live hits so
  replayed contacts re-resolve deterministically through the query path.

The guest now issues ZERO hit queries on the live touch path.

Validated: core 110 (bounds/carry/decode/hitPass new), contract byte-equal,
gesture+touch 23, virtual-list 16 (incl. the query-fallback case), im-sim 11
(the OSK tap journey), canonical chain, 52 web goldens byte-exact, Vita3K
e2e 52/52 byte-exact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@doodlewind
doodlewind marked this pull request as ready for review August 3, 2026 07:45
@doodlewind
doodlewind merged commit 63ab281 into main Aug 3, 2026
2 checks passed
@doodlewind
doodlewind deleted the feat/touch-hit-facts branch August 3, 2026 07:45
doodlewind added a commit that referenced this pull request Aug 3, 2026
…ardware bug (#213)

Every touch on real Vita hardware resolved to ONE node — the TextField's
Portal host — killing list pans and field taps while the OSK (modally
exempt) kept working. The host is an explicit full-screen absolute box in
the overlay layer; #209 marked the overlay ROOT hitPass but every portal
host inside it was another full-screen pure container that still claimed
every bounds hit.

Why no rig caught it: the pocket-youtube sim's vendored wasm was STALE
(predating op 42), so its journeys silently exercised the ink-query
FALLBACK, not the fact path; the Vita3K suite's only touch spec drove OSK
keys — overlay content, immune. The on-device svc diagnostic (every
touch-down reporting x,y,hit) showed the constant node id across the whole
screen; rebuilding the vendor wasm reproduced it in the sim in one probe.

- Portal host + the TextField dock wrapper: hitPass (pure plumbing never
  claims; a modal scrim is portal CONTENT and keeps blocking).
- tests/portal-hit.test.ts: the REAL-core mount test mock hosts cannot
  express — a mounted empty-chrome Portal must not swallow bounds hits
  (fails without the fix, registered in the chain).
- golden-specs: im-rowtap made permanent — app-layer tap-through-the-stack
  coverage on BOTH oracles (web + Vita3K), the hole this bug lived in.

Validated: canonical chain 0 fail, 54 web goldens, im-rowtap + im-touch
Vita3K byte-exact.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant