Skip to content

test(android): discriminator proves the in-process 'eye swap' is an interlace-PHASE error, not a view-order bug - #1089

Open
dfattal wants to merge 1 commit into
mainfrom
fix/inprocess-eye-order
Open

test(android): discriminator proves the in-process 'eye swap' is an interlace-PHASE error, not a view-order bug#1089
dfattal wants to merge 1 commit into
mainfrom
fix/inprocess-eye-order

Conversation

@dfattal

@dfattal dfattal commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

TL;DR — it is not an eye swap

cube_zones_vk_android in-process on the NP02J is not feeding the wrong view to
the wrong atlas tile. The view→tile order and the runtime's render-ready view poses
are bit-identical between the in-process and OOP routes. What differs is the
interlace phase, by an odd number of pixels — which inverts the per-pixel view
selection and therefore reads on the panel as a perfect left/right swap. The human's
own caveat ("small possibility of a 0.5 phase shift") was right.

The discriminator (this PR)

A genuine view→tile swap and an odd-pixel phase error look identical on glass, so
this PR adds two knobs to the zones test app that separate them, hardware-in-the-loop,
with no human:

  • debug.dxr.tintviews 1 — after the scene draw, overwrite each atlas tile with a
    flat colour keyed to the source view (0 = red, 1 = blue, 2+ = green). A
    adb exec-out screencap of the woven output then reveals, per subpixel, which
    view the weaver picked. Cross-correlating the R−B signal along a row between two runs
    gives a quantitative answer: +1 = same phase, negative = polarity inverted =
    eyes swapped. One vkCmdClearAttachments per tile; off by default.
  • EYEORDER — logs the sign of views[1].x − views[0].x from the runtime's
    render-ready rig. Answers the eye-order question independently of the weave and of
    where the face happens to be. Logged once, then only on a sign flip.

Making the weave deterministic is the trick that makes screencaps comparable at
all: the interlace phase tracks the tracked face, which jitters ~±0.05 m ≈ ±0.8 of a
lens period, randomising every capture. debug.dxr.overlay 1 (forces NoFaceMode off,
3D on) plus cmd sensor_privacy enable 0 camera gives a live weave with a frozen
default viewer — self-correlation 1.0000 across repeat captures.

Measured on NP02J (runtime v1.21.1-753-g839a4ea90, display-config 0.10.62)

case in-proc vs OOP, corr@0 verdict
fullscreen +0.993 … +1.000 (best lag 0) bit-identical, both correct
freeform, WM origin (0,0) +0.68 same polarity, correct
freeform, WM origin (300,500) −0.552 … −0.642 (aligns at lag +3 px) polarity inverted → pseudoscopic

EYEORDER is byte-identical on both routes in every case
(v0.x=+0.0141 v1.x=+0.2407 dx=+0.2266), confirming no eye-order bug anywhere.

Root cause

The only route-dependent input to the interlace phase is the window screen rect:

  • in-process — vk_update_present_origin()
    (src/xrt/compositor/vk_native/comp_vk_native_compositor.c:5857-5881) reads
    android_globals_get_window_screen_rect() and calls
    xrt_display_processor_vk_set_window_screen_rect(x, y, …). The publisher is the
    app's own xrSetAndroidWindowGeometryDXR (Architecture A spike: in-app Android compositor (real XR_DXR_android_surface_binding, async main-thread DP creation, host classloader, AAR <queries><intent>) #1037), fed from
    View.getLocationOnScreen(). Plug-in then does
    set_viewport_screen_position(win_x, win_y) → log line
    weave full-target screen-pos 300,500 target 1000x1500 (#150).
  • OOP — android_globals is process-local. The app's publish lands in the app's
    process and never reaches the service, so comp_multi's update_window_screen_rect
    leaves the origin at (0,0); the plug-in logs
    weave band 332,374 500x750 screen-pos 332,374 (zonephase=1) target 2560x1600 (#53).

Fullscreen hides the divergence because the origin is (0,0) on both sides — which is
exactly why the human's fullscreen eyeball looked good.

…and why the repro itself is invalid

The staged am task resize recipe (staged715.sh / ff.sh / android-sidebyside.sh)
moves the task bounds but not the composited window on this device. Both
authorities agree, and neither changes when the WM bounds do:

  • SurfaceFlinger: displayFrame=[0 1560 1500 2560] sourceCrop=[0 0 1000 1500] bufferTransform=ROT_270
  • InputDispatcher: frame=[0,1560][1500,2560]

…identical at WM (300,500) and at WM (900,100). The woven content lands at
framebuffer (332,374) — i.e. window(0,0) + zone(332,374) — in both cases, verified
by the tint bbox (x=332..830 y=374..1122).

So under this recipe the app correctly reports the WM bounds, the WM bounds are not
where the window is, and the in-process leg anchors the lens phase to a place the window
isn't. OOP looked correct only because it applies no window origin at all and the
window really was at (0,0).

No weave fix is shipped here, deliberately: with a real drag (WM bounds and
SurfaceFlinger agreeing) the in-process leg is the one that is right by design and the
OOP leg is the one that would weave at a stale (0,0). Picking a side needs a
hand-dragged repro, not a am task resize one.

Latent defect this surfaced

android_globals being process-local means the OOP route never receives the app's
window rect at all
— any non-fullscreen OOP client weaves (and places its zone band)
at panel origin (0,0). Fullscreen masks it. Worth its own issue against #1033.

Eyeball recipe for the human

adb shell setprop debug.dxr.force_ipc 0
adb shell setprop debug.dxr.tintviews 0
# launch cube_zones_vk_android freeform, then DRAG the window by its title bar
# (do NOT use `am task resize` — it does not move the surface on this device)
adb shell dumpsys input | grep -A1 cube_zones | grep -o 'frame=\[[0-9,]*\]\[[0-9,]*\]'

Confirm the reported frame= actually tracks the drag, then judge depth polarity: the
crate must come toward you and the floor grid recede. If it is inverted, capture the
HW_DBG_CNSDK: weave … screen-pos line at that moment — that is the phase origin in
question.

Refs #1031 #1074

🤖 Generated with Claude Code
Session: https://claude.ai/code/session_01N3kzh1GTPNrm62TbEhFmqP

…nes_vk_android

The "in-process Android weave shows SWAPPED left/right eyes" report cannot be
settled by eyeballing the panel: a genuine view->tile swap and an interlace
phase error of an odd pixel look IDENTICAL (both read pseudoscopic). These two
knobs separate them, on-device, without a human.

* `debug.dxr.tintviews 1` overwrites each atlas tile with a flat colour keyed
  to the SOURCE VIEW (0 = red, 1 = blue, 2+ = green) after the scene draw. A
  screencap of the WOVEN output then shows, per subpixel, which view the
  weaver selected -- so `adb exec-out screencap` becomes a quantitative phase
  probe (cross-correlate the R-B signal along a row between two runs: +1 =
  same phase, negative = polarity inverted = eyes swapped). One
  vkCmdClearAttachments per tile, off by default.

* `EYEORDER` logs the sign of (views[1].x - views[0].x) from the runtime's
  render-ready rig -- the eye-order question answered independently of the
  weave and of where the face happens to be. Logged once, then only when the
  sign flips.

Measured on NP02J with the face frozen (`debug.dxr.overlay 1` + camera
privacy => NoFaceMode off but no tracking, so the weave is deterministic;
self-correlation 1.0000):

  fullscreen, in-proc vs OOP      corr@0 = +0.99..+1.00   (bit-identical)
  freeform @ WM(0,0),  vs OOP     corr@0 = +0.68          (same polarity)
  freeform @ WM(300,500) vs OOP   corr@0 = -0.55..-0.64   (polarity INVERTED)

and EYEORDER is bit-identical across both routes. So it is NOT an eye-order
bug: it is the window-origin term in the interlace phase, which only the
in-process leg applies (`android_globals` is process-local, so the app's
`xrSetAndroidWindowGeometryDXR` publish reaches the DP in-process and never
reaches the service).

Refs #1031 #1074

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3kzh1GTPNrm62TbEhFmqP
@dfattal

dfattal commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Human eyeball (2026-08-20): PASS. Fresh freeform launch, in-process: correct depth (crate toward viewer, grid recedes), clean weave, tracking live — confirming the phase-inversion was entirely the #1087 am task resize WM↔SF desync, and the in-process phase anchoring is correct by design. Also learned: this ZTE freeform implementation has no window title bar and no user drag — windows are placed at launch/programmatically only, so the drag-time phase question cannot arise for users on this device.

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