Skip to content

feat(stereo-camera): R3 — consent, foreground rule, indicator, kill switches, keyed persistentId (ADR-043) - #1819

Merged
dfattal merged 37 commits into
mainfrom
feat/stereo-camera-r3
Oct 6, 2026
Merged

dfattal merged 37 commits into
mainfrom
feat/stereo-camera-r3

Conversation

@dfattal

@dfattal dfattal commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

Phase R3 of the stereo camera source (ADR-043): consent and privacy, vendor-neutral — the service enforces it, the plug-in never sees it. Stacked on #1747 (R2), which stacks on #1746 (R1); this PR's own commits start at 2146e1231.

What is implemented

  • Camera-only client class CAMERA_CONSUMER (value 6), declared with XrStereoCameraClientInfoDXR + XR_STEREO_CAMERA_CLIENT_CONSUMER_ONLY_BIT_DXR on XrInstanceCreateInfo. Verified by use (camera handlers only, xrCreateSession refused), quota 4, never counted toward the PRESENT_OWNER quota — the owner-counting rule is now the pure u_client_class_present_owner_count() (tested). The tester's sibling rule for the browser's other processes is kept. A hybrid runtime always routes a declared consumer to the service.
  • Consent policy + store (u_camera_consent.{h,c}, u_camera_consent_store.c): sharing off → dev override → no identity → registered delegating client (HKLM / HKCU CameraConsent\Delegating, /etc/displayxr/camera-delegating.json, /Library/Application Support/DisplayXR/…, or displayxr-cli camera trust) → Windows ConsentStore\webcam → stored Allow/Deny (HKCU\Software\DisplayXR\CameraConsent\Apps, camera_consent.json 0600) → Allow-once (per pid) → tray prompt " wants to use the 3D camera — Allow / Allow once / Deny" (blocks the start ≤ 60 s; unanswered = refused, retryable). DXR_STEREO_CAMERA_DEV_ALLOW=1 stays as the documented, logged dev override; DXR_STEREO_CAMERA=0 the kill switch; new DXR_STEREO_CAMERA_PROMPT=0 for headless services.
  • Distinct results (spec v2, appended at 1004999311–316): XR_ERROR_STEREO_CAMERA_CONSENT_REFUSED_DXR (−1004999312, → NotAllowedError), _DISABLED_DXR (−…313), _BUSY_DXR (−…314), _STREAM_ENDED_DXR (−…316); XR_TYPE_STEREO_CAMERA_CLIENT_INFO_DXR (…311), XR_TYPE_EVENT_DATA_STEREO_CAMERA_STREAM_ENDED_DXR (…315). xrt side: XRT_ERROR_STEREO_CAMERA_* −45…−48. PERMISSION_INSUFFICIENT now means the OS switch / wrong class.
  • Foreground rule + lock suspension: window-bearing classes need a visible top-level window of the peer pid (EnumWindows / NSRunningApplication; Linux always visible); delegating / CAMERA_CONSUMER / DIAG exempt. Every stream suspends on session lock (Windows WM_WTSSESSION_CHANGE + SM_REMOTESESSION; macOS screenIsLocked + sessionDidResignActive); camera fake-lock for tests.
  • Events delivered: per-connection queues (stereo_camera_poll_event) drained from xrPollEvent — StateChanged (effective state: SUSPENDED while locked / sharing off) and the new StreamEnded (handle + reason).
  • Indicator + kill switches: Windows tray icon badge, tooltip/menu "3D camera in use by ", balloon; macOS menu-bar badge + lines. "Stop camera sharing" ends every stream (STREAM_ENDED); "Share the 3D camera with apps" persistent toggle (zero cameras, DISABLED).
  • Keyed persistentId: HMAC-SHA-256 under a per-user 32-byte secret kept in the store (compact u_sha256, FIPS / RFC 4231 vectors).
  • Read-only consumer section on every platform (ipc_shmem_create_with_readonly: FILE_MAP_READ dup / O_RDONLY re-open before unlink / ASharedMemory_setProt).
  • CLI: camera consent | allow | deny | revoke | trust | untrust (<exe>|--self) (local), status | stop-all | sharing on|off | fake-lock on|off (IPC, DIAG); probe prints events and exits 4/6/7/8.
  • Docs: spec §2a/§7/§8/v2, roadmap R3 as built, env census, docs/guides/stereo-camera-consent.md, ADR-043 Amendment 1.

Evidence (macOS, sim fake, headless prompt)

tests_camera_consent: 105 assertions / 11 cases; tests_stereo_camera, tests_ipc_proto green; MinGW aux_util (registry + bcrypt store) compiles. End to end with SIM_DISPLAY_FAKE_STEREO_CAMERA=1 DXR_STEREO_CAMERA_PROMPT=0:

probe (no consent)      -> stream start failed: -45 = CONSENT_REFUSED            exit 4
calib (no consent)      -> -45 CONSENT_REFUSED
camera allow --self     -> probe: 61 frames @ 29.95 Hz, events WAITING, AVAILABLE
list                    -> persistentId dxrcam-1d4fb697c0143b4d51cd9a1090bcfb20 (keyed)
revoke --self           -> -45 again; trust --self -> allowed as "registered consent-delegating client"
fake-lock on/off        -> [2.30 s] camera 1 state -> SUSPENDED, 56 source frames not delivered,
                           [4.20 s] -> AVAILABLE, delivery resumes
stop-all                -> acquire -48 STREAM_ENDED, [2.03 s] stream ENDED (USER_STOPPED), exit 8
sharing off / on        -> 0 stereo camera(s) / 1 stereo camera(s)

Needs the Windows box

  • Build + run service_tray_win.c: badge icon, consent dialog, WTS lock/unlock, SM_REMOTESESSION; the ConsentStore\webcam read against the real Settings switch; manual matrix allow / deny / revoke / OS switch off / lock screen / background app.
  • ipc_server_stereo_camera.c Windows branches (EnumWindows foreground rule, FILE_MAP_READ section).

Follow-ups

  • B1 (browser-pvt): chain XrStereoCameraClientInfoDXR (CONSUMER_ONLY) on the capture utility's instance instead of enabling XR_DXR_weave to be classed as the browser; map CONSENT_REFUSED → NotAllowedError, BUSY → NotReadableError, DISABLED → hide the device; handle XrEventDataStereoCameraStreamEndedDXR as track end.
  • Installer: register chrome.exe under HKLM\Software\DisplayXR\CameraConsent\Delegating (REG_SZ data = full path).
  • Android consent (A1); a signer check on delegating entries.

🤖 Generated with Claude Code

dfattal pushed a commit that referenced this pull request Oct 6, 2026
…ompt cursor type

Two findings from the R3 hardware run on the SR laptop (#1819):

- 'camera sharing off' then 'camera probe' exited 3 ('the service exposes no
  stereo camera') instead of the documented 6. While sharing is off the service
  enumerates no camera, so the probe never reached the stream-create refusal
  that maps to 6. The probe now asks the service's status (a DIAG-only control,
  so over a second short-lived connection) when the enumeration is empty, and
  reports DISABLED / exit 6 when sharing is off. A machine with no stereo
  camera still exits 3.
- service_tray_win.c: LoadCursorW(NULL, IDC_ARROW) is MSVC C4133 in this
  non-UNICODE build (IDC_ARROW is the ANSI-typed MAKEINTRESOURCE); use
  MAKEINTRESOURCEW(32512).

Compiled on macOS (displayxr-cli, displayxr-service). NOT run: the exit code
needs a service with the sim or SR camera and a verified DIAG CLI; the tray
file is Windows-only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dfattal

dfattal commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Hardware gates on the SR laptop — PASS (run by the Windows session, 2026-10-05)

Runtime R3 84125d183 + Leia L1 e9fe9f7 + browser B1 02a707b, service without DXR_STEREO_CAMERA_DEV_ALLOW:

  • MSVC clean; unit tests pass (camera_consent 11/11, camera_profile, stereo_camera, rectify, vrefine, lift_mailbox); selftest passes (camera + lift).
  • CLI: stored deny → exit 4; stop-all on a live probe → exit 8; fake-lock pauses frames and they resume.
  • probe --rectified: pass with the room lit (signed dy +0.12 px, vrefine 131 updates, +1.12 → +0.02 px). In a dark room the refinement has nothing to match (0 windows) — a scene limit, same code as R2.
  • Browser: the 3D camera is listed, the tracker is hidden, 1280×480 rectified at ~58 Hz; the service shows the client as CAMERA_CONSUMER; one tray prompt → Allow → stored. Deny → NotAllowedError; sharing off → NotReadableError; stop-all → track ended; fake lock → frames freeze, track stays live, resumes.
  • Live 3D call over the hosted signalling with the stereo camera: connected, rectified 1280×480 VP9 ~30 fps, 0 dropped, auto-convergence tracking.

Two findings from that run are fixed in b567f3203: camera sharing off + camera probe exited 3 instead of the documented 6; and an MSVC C4133 on the tray prompt's cursor. Both compiled on macOS, not run — the Windows session is asked to re-check them with its next build.

Not run: components_unittests for the browser side (the build artifact ships the browser only).

dfattal and others added 28 commits October 6, 2026 00:41
…04999300-310, catalog note (ADR-043)

Type values sit at 1004999300-310 (not 290-300 as first drafted): on the
rebase onto post-lift main, 290 had been taken by XR_TYPE_WEAVE_SUBMIT_OVERLAY_UNCHANGED_DXR
(weave spec v14). Safe to move: spec v1, nothing shipped chains these.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ator, NV12/BGRA/GRAY8 conversion, persistent id, disparity probe

Pure, OS-free building blocks shared by the service camera manager, the
sim_display fake and displayxr-cli camera (ADR-043).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_STEREO_CAMERA) + sim_display FAKE stereo camera

Six optional slots appended to xrt_plugin_iface per ADR-020 (struct_size-gated,
no ABI bump): enumerate, get_calibration (OpenCV R,T of the ACTIVE device),
open (= tracker keep-alive), wait_frame, release_frame, close.

They sit after a one-pointer placeholder for ADR-042's create_dp_d3d11_lift,
which claims the offset right after the vk fingerprint on feat/lift-ext but is
not on main yet: the camera slots are therefore at their final offsets whichever
branch merges first, and tests_stereo_camera asserts the adjacency (against the
real lift slot once it exists).

sim_display fills the slots; SIM_DISPLAY_FAKE_STEREO_CAMERA=1 advertises one
640x480-per-eye 30 Hz GRAY8 camera rendering a random-dot pair with a 12 px
background and a 40 px bar (68 deg HFOV, 50 mm, 2.0/0.6 m) plus a frame counter;
_SIZE / _FPS / _FORMAT / _SUSPEND_PERIOD_MS knobs.

xrt_instance gains the stereo_camera client aspect (IPC instance only) and
get_active_plugin (native instance) for the service's camera manager.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mera_* calls)

ipc_server_stereo_camera: the service opens each plug-in camera once, on the
first stream start, and closes it 2 s after the last stop. One runtime-owned
thread per open camera pulls wait_frame and fans each frame out to every
started stream: per stream a 3-slot latest-wins pinned ring in shared memory
(u_stereo_camera_ring), format conversion, decimation, and a per-stream wake
handle (auto-reset event on Windows, non-blocking pipe on POSIX). States
WAITING / AVAILABLE / SUSPENDED / UNAVAILABLE follow the plug-in; a suspension
clears every pin. Streams are owned by the connection and die with it.

Privacy hooks, deny by default: start and calibration reads are refused
(XRT_ERROR_NOT_AUTHORIZED) unless DXR_STEREO_CAMERA_DEV_ALLOW=1 is set in the
SERVICE environment (R3 replaces it with OS + DisplayXR consent);
DXR_STEREO_CAMERA=0 is the kill switch (zero cameras); RAW is refused to
PRESENT_OWNER clients (the browser); the foreground rule and in-use indicator
are marked hook points. Windows hands out a FILE_MAP_READ section duplicate and
a SYNCHRONIZE-only event duplicate. The D3D11 / AHardwareBuffer transports are
designed in the file comment (same ring, texture slots + fence / sync_file).

Client side: ipc_client_stereo_camera (connection-level calls) and the
xrt_stereo_camera_client aspect the IPC instance now carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n its open time

Re-anchoring the pacing clock after a pause also moved the suspend phase, so
every SUSPENDED half-period stretched (36 frames in 5 s instead of ~75 at a
1 s period). Separate origins; now 92 frames in 6 s, 3 suspensions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A real DIAG consumer over IPC: probe creates + starts a stream (through the
service's authorisation), maps the ring READ-ONLY, waits on the per-stream wake
handle, acquires, and reports the measured delivery rate, frame-index gaps, the
SBS layout, block-matched disparities of the last frame and the service's
stream stats; --out writes the last frame as a PNG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce-level, service-only)

Nine entry points over the instance's xrt_stereo_camera_client aspect. An
in-process instance enumerates zero (maintainer decision: service clients
only in R1). Consent refusal maps to XR_ERROR_PERMISSION_INSUFFICIENT
(retryable), a dead pipe to XR_ERROR_INSTANCE_LOST; capture times are converted
to XrTime; handles returned through XrStereoCameraStreamTransportDXR are the
caller's (closed here when nothing is chained to receive them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enumerates the active plug-in's cameras through the iface and validates each
description (even non-zero eye extent, known format, positive rate, CALIBRATED
backed by a real calibration). Absence never fails.
sim_display: 'PASS: stereo_camera_caps — 0 cameras (OK)'; with the fake:
'1 camera(s); [0] "Sim 3D camera (fake)" 640x480/eye @ 30.0 Hz, flags 0x1f'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stant expressions for MSVC/GCC)

Caught by the MinGW syntax check: the XR flag bits are static const
XrFlags64, which only clang accepts in _Static_assert. Pin the xrt mirrors to
the header's literal values at compile time and cross-check the real constants
once at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oid facts

- spec: status R1, the DXR_STEREO_CAMERA_DEV_ALLOW dev gate, privacy hook
  status, slot placement behind the lift placeholder, the OpenCV R,T plug-in
  convention and the wait_frame result enum, the fake as built, CLI output.
- roadmap: §C Android rewritten from the NP02J/K68 measurements (logical
  cameras 4/5, two raw 1280x720 NV12 streams, LeiaCameraBuilder is not CNSDK,
  opening the front pair evicts the tracker for good), design consequence: the
  runtime owns the pair and runs in-app tracking from the same capture;
  §E R1 as built + merge order; §F Android risk + CNSDK asks; §G the four R1
  maintainer decisions.
- ADR-043: R1 status, Android context/consequence updated.
- xrt_plugin_iface reference: the stereo_camera_* slot contract.
- env census: DXR_STEREO_CAMERA, DXR_STEREO_CAMERA_DEV_ALLOW.
- ADR index regenerated (it was stale on the docs branch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…SVC C5287)

MSVC /W4 raised C5287 ("operands are different enum types") on the
XrStereoCamera*DXR vs xrt_stereo_camera_* _Static_asserts, even through the
(int) casts. Pin each side against the header's literal instead: two
enum-vs-literal checks carry the same proof with no cross-enum operator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ver a guess

Field test (Leia SR tracking camera): the descriptor said "up to 30.0 Hz" while
the source ran 58-64 Hz — the plug-in's enumerate-time rate was a hard-coded
placeholder. The service now measures every source over the first 60 frames
after each open (u_stereo_camera_rate_meter; a >0.5 s gap restarts the window)
and the descriptor, the decimator and the stream info use that; until a window
completes they use the plug-in's value, where 0 now means "unknown" (xrt_plugin
doc, XR header doc, spec). One WARN on the first measurement, INFO after.

- selftest: max_frame_rate 0 is legal; negative / NaN / > 1000 is malformed.
- camera list prints "rate unknown (measured once a stream runs)"; probe prints
  "cap = source rate" instead of "cap 0.0 Hz".
- sim fake: SIM_DISPLAY_FAKE_STEREO_CAMERA_ADVERTISED_FPS (0 = unknown) to
  exercise the measured path; tests_stereo_camera covers the meter (jitter,
  suspension gap, restart keeps the last value).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, not a fixed 64 px

On the Leia SR tracking camera (baseline 120 mm, f ~513 px) a face at 0.6 m is
~95 px of disparity, so every block of the fixed 0..64 px search clamped at 64
and the probe reported "d = 64". The bound is now f*B/0.4 m from the camera's
own baseline + HFOV (quarter eye width when uncalibrated; clamped to
[64, 0.6*eye]), and a block whose best match sits ON the bound is counted and
excluded ("AT THE SEARCH BOUND") instead of silently reported as the bound.

sim fake: SIM_DISPLAY_FAKE_STEREO_CAMERA_BASELINE_MM (default 50). At 120 mm
the probe reads 28 px (background) / 95 px (bar at 0.6 m), search 0..143.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…slots after platform-state, values 300-310

Mechanical follow-through of the rebase onto main (lift v2.22.0 + ADR-045):
- docs no longer describe the reserved_adr042_create_dp_d3d11_lift placeholder;
  the camera slots append after create_dp_d3d11_lift AND get_platform_state.
- type values quoted as 1004999300-310 everywhere (290 is weave v14's).
- cli CMake: one stb include for lift + camera, not two.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…emap, no OpenCV (R2)

Vendor-neutral rectifier for XR_DXR_stereo_camera (ADR-043 R2), in three
layers so a GPU path can replace only the last one:

- geometry: OpenCV's stereoRectify(CALIB_ZERO_DISPARITY, alpha = 0) in C
  (half rotations + baseline onto x, min-fy focal, corner-centroid principal
  point, 9x9 inner-rectangle zoom), plus a border check of the real maps that
  zooms further until no output pixel samples outside its raw image;
- float maps (what a GPU backend uploads as an RG32F texture);
- CPU backend: fixed-point bilinear taps over the whole SBS image, 1/2/4
  channels (GRAY8, NV12 Y + half-res UV, BGRA8).

RADTAN5 / RADTAN8 / KB4 lens models, calibration rescaled when it was taken
at a different size than the frames. Also u_stereo_camera_estimate_offset:
SSD block matching over dx AND dy, for row-alignment checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd truth (R2)

SIM_DISPLAY_FAKE_STEREO_CAMERA_DISTORT=1 renders the fake's scene through two
RAW cameras: different per-eye intrinsics, RADTAN5 barrel lenses, and each
camera turned by half of (pitch 0.35, yaw 0.25, roll 0.45 deg) in opposite
senses (~6 px vertical misalignment, 0.9 deg relative roll). The symmetric
split makes Bouguet recover exactly the virtual parallel pair the scene is
defined in, so the ground truth after rectification is exact: aligned rows and
disparity f_rect * B / Z. The camera drops NATIVELY_RECTIFIED and its
calibration slot returns the truth, so RECTIFIED output exercises the
service's rectifier. The scene is sampled continuously (bilinear value noise),
so resampling is honest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the distorted fake

tests_stereo_rectify, with fixtures generated offline by
tests/fixtures/gen_stereo_rectify_fixtures.py (OpenCV 4.10) for three
calibrations (the sim fake, an SR-like pair with a non-axial baseline, a
RADTAN8 pair calibrated at 2x the frame size):

- R1/R2 equal OpenCV's to 1e-12; principal point equals the converged
  reference to 1e-4 px (OpenCV's own P1 within 0.75 px: it stops
  undistortPoints after 5 iterations); focal within 1 % (measured +0.05..0.13 %);
- maps equal initUndistortRectifyMap to 3e-5 px;
- alpha = 0 really leaves no black pixel (full and half resolution);
- end to end: raw |dy| median 2.9 px -> rectified worst block 0.061 px;
  disparity 11.33 / 37.77 px ground truth, worst block error 0.063 px;
- NV12 / BGRA8 planes agree with GRAY8; hidden [.perf] case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fied calibration (R2)

A per-camera rectifier is built at manager create from the plug-in's RAW
calibration when it is CALIBRATED and not NATIVELY_RECTIFIED. It runs once per
source frame on the camera thread, outside the manager lock, and only while a
started stream wants RECTIFIED; RAW streams read the original frame. A
RECTIFIED stream never receives unrectified pixels (a frame produced before it
started is skipped).

get_calibration(RECTIFIED) returns the same geometry: one pinhole for both
eyes (zero disparity at infinity), no distortion, rightFromLeft = identity +
(+baseline, 0, 0). horizontalFovDeg is the rectified one. The browser
(PRESENT_OWNER) is refused a camera the rectifier rejected instead of getting
the RAW-flagged fallback native clients get. The seam is struct scam_rectifier
(backend-neutral geometry + an apply() backend: CPU LUT today).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lignment, --rectified gates it

Every probe now 2-D block-matches the last frame (textured 32x32 blocks) and
prints |dy| median / p90 / blocks above 1 px, plus the dominant disparities as
depths Z = f*B/d from the RECTIFIED calibration. probe --rectified asks for
RECTIFIED output and exits 5 when frames come back RAW or the median |dy|
exceeds 0.5 px. Against the distorted fake: median 0.016 px, depths 2.004 m
and 0.599 m (truth 2.0 / 0.6); --raw shows the 2.6 px it fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…measured numbers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… camera at -x

A swapped SBS pair still rectifies (Bouguet does not care), but every
disparity comes out negative and rightFromLeft points the wrong way. The
L1 Leia provider decides eye order from the sign of T; this makes a wrong
decision visible in the service log instead of only in a probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MSVC only defines M_PI when _USE_MATH_DEFINES precedes the first
<cmath>, which Catch may already have pulled in. Use a local constant.
First MSVC build of R2; the test passes (716 assertions, 7 cases).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, signed dy, bound hits flagged

First SR-hardware run: a face at 0.6 m on the 120 mm tracker is ~95 px of
disparity, past the probe's 0..64 search, and the SSD matcher's +-10 px dy
window clamped too — it reported "d = 64, |dy| = 10.000" as if measured.

- u_stereo_camera_match_block: zero-mean NCC (blind to the two sensors'
  gain/offset), coarse pass on every 2nd pixel + full-res hill-climb, parabolic
  sub-pixel on both axes, the searched window returned, and dx_at_edge /
  dy_at_edge when the integer peak sits ON a bound (a clamp, not a measurement).
- camera probe: disparity window f*B/0.4 m from the camera (R1 helper), dy
  +-24 px, accept NCC >= 0.90; prints accepted / low-NCC / at-bound counts,
  warns when > 20 % of blocks hit a bound, reports the SIGNED dy median, and
  --rectified now also fails on a constant offset (|signed median| > 0.5 px).
  --max-disparity / --max-dy / --min-ncc override.
- sim fake: the DISTORTED variant honours _BASELINE_MM too (truth + slot).
- tests: large-baseline matcher (bar 94.9 px, signed dy +1.68 recovered to
  0.07 px, gain/offset, flat refused, the old 64 px window is REJECTED not
  reported); END TO END at 120 mm (bar 90.64 px GT, measured 90.66); the
  [vshift] hypotheses for the field's +1.7 px residual (per-eye cy error =
  constant dy ~ -dcy, transposed R = large roll-shaped dy, calib-size rescale
  and the plug-in's half swap exact, identity = pure scale about cy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…torted fake (R2)

SIM_DISPLAY_FAKE_STEREO_CAMERA_DY (px) and _DY_SLOPE (px per 100 px) shift the
right eye's rows on top of the DISTORTED pair without telling the reported
calibration, like a device whose stored calibration is slightly off (the Leia
SR laptop: +2.6 px, -0.47 px / 100 px). Drives the online refinement's tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reo_vrefine)

On a Leia SR laptop the rectified rows stayed +1.71 px apart, and OpenCV's
stereoRectify reproduces that on the same frame: the stored calibration is off,
not the rectifier. u_stereo_vrefine measures the residual dy = a + b (y - cy)
on rectified frames (Shi-Tomasi patches, half-res NCC search, full-res NCC
refinement, gated on NCC, d > 0, unclamped peaks and vertical curvature), fits
it robustly (median + Tukey IRLS) and runs the controller: duty cycle, >= 50
matches over >= 3 frames, 0.2 px deadband, |a| <= 6 px, |b| <= 2 px/100 px.

u_stereo_rectify folds the correction in as v_offset = a/f, v_slope = b: a
symmetric per-eye vertical affine in normalized rectified coordinates applied
BEFORE the principal point and the alpha = 0 crop, so P1/P2 describe the
corrected frames. Adds u_stereo_rectify_rect_from_raw (the inverse map).

tests_stereo_vrefine: the fake with the SR model converges to +0.000 px in one
rebuild; degenerate frames never apply; clamps hold; and a REAL SR raw frame
(62 KB JPEG + calibration + 63 OpenCV SIFT matches as an independent judge):
signed dy median +1.715 px -> +0.075 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reports it

A per-camera worker measures rectified frames the camera thread parks for it
(every 250 ms for 5 s after an open or an update, then every 30 s; 2.7 ms per
measurement at -O2, off the frame path), rebuilds geometry + LUTs when the
correction moves, and the camera thread swaps them in between frames and bumps
calibration_generation. WARN on first application, INFO after, re-verified on
re-open, DXR_STEREO_CAMERA_REFINE=0 kill switch. Stream stats carry the state,
applied a/b, match count and dy before/after; camera probe prints them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dfattal and others added 9 commits October 6, 2026 00:41
… |dy| median

On the SR laptop with a face at 0.6 m the refined rows were aligned (signed median
-0.10/-0.18 px) but --rectified FAILED: block-match noise alone put the unsigned
|dy| median at 1.03/1.10 px. Gate on |signed median| <= 0.5 px, MAD about it
<= 1.5 px and >= 20 blocks; print the gate numbers; keep |dy| stats informational.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions

On the SR laptop the B1 browser's "3D Camera (DisplayXR)" never appeared:
[CAP] refusing client pid=... ('ChromiumStereoCamera'): class PRESENT_OWNER
quota 2/2 exhausted (#960). One browser already takes both slots (GPU
process weaves, browser process binds the window), so its video-capture
utility - the XR_DXR_stereo_camera client, which declares PRESENT_OWNER
by enabling XR_DXR_weave - was the third connection and was refused
(xrCreateInstance -10 in the capture service).

A connection from the same executable as an admitted PRESENT_OWNER is now
a sibling and takes no new slot; the quota is compared against DISTINCT
owner executables, so two different browsers still fill it. A peer whose
image path is unreadable is never a sibling (fails closed).

Verified on the SR laptop: all three Chromium processes admitted, the
page lists and opens "3D Camera (DisplayXR)" (sbs 1280x480), stream
RECTIFIED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…stentId (ADR-043 R3)

u_camera_consent: the spec §7.1 decision tree (sharing off > dev override >
no identity > delegating client > OS camera switch > stored decision >
Allow-once > prompt) over two injected vtables so it is testable with fakes.
u_camera_consent_store: the real store — HKCU\Software\DisplayXR\CameraConsent
on Windows (plus HKLM / HKCU Delegating lists and the CapabilityAccessManager
webcam switch), camera_consent.json (0600) in the user config dir on POSIX
plus the installer's system delegating list. persistentId becomes
HMAC-SHA-256 under a 32-byte per-user secret the store generates once
(u_sha256: compact FIPS 180-4 / RFC 2104, pinned by test vectors).
u_client_class: the PRESENT_OWNER owner-counting rule as a pure function.

tests_camera_consent covers every rule, Allow-once bound to the pid, prompt
timeout / unavailable retried, the keyed id's stability and unlinkability,
and the owner count with a CAMERA_CONSUMER sibling.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…de the panel quota

A browser's video-capture utility (B1) has no window and no weave, yet was
classed PRESENT_OWNER by enabling XR_DXR_weave and spent a panel-owner slot.
XRT_CLIENT_CLASS_CAMERA_CONSUMER is declared explicitly with the new
XrStereoCameraClientInfoDXR (CONSUMER_ONLY bit) on XrInstanceCreateInfo,
verified by use (camera handlers only; session_create refuses it), quota 4,
and never counted toward the PRESENT_OWNER quota — the handler now uses the
pure u_client_class_present_owner_count(). A hybrid runtime always routes a
declared consumer to the service. The tester's owner-quota (sibling) rule
for the browser's other processes is kept.

XR_DXR_stereo_camera spec v2: appends 1004999311–316 (client-info struct,
CONSENT_REFUSED / DISABLED / BUSY / STREAM_ENDED results, the stream-ended
event); nothing renumbered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…k; distinct results; events delivered

Replaces the R1 DXR_STEREO_CAMERA_DEV_ALLOW gate with the u_camera_consent
policy at every stream start and calibration read, evaluated with the
manager lock released and one evaluation at a time (the tray prompt blocks
the client thread for up to 60 s). Each verdict has its own result —
XRT_ERROR_STEREO_CAMERA_CONSENT_REFUSED / _DISABLED / _BUSY / _STREAM_ENDED,
mapped 1:1 to the new XrResults — so a browser can map consent to
NotAllowedError. RAW is refused to PRESENT_OWNER and delegating clients.

Foreground rule per published frame (250 ms cache): window-bearing classes
need a visible top-level window of the peer pid (EnumWindows / NSRunning-
Application); delegating, CAMERA_CONSUMER and DIAG are exempt. Every stream
is suspended while the OS session is locked (set_session_locked, fed by the
platform hooks); the reported state is SUSPENDED meanwhile.

The state-changed and stream-ended events are now delivered: per-connection
queues drained by stereo_camera_poll_event from xrPollEvent, mapping a
service stream id back to the app's handle. stop_all / set_sharing /
get_status serve the UI; the macOS menu-bar item gains the in-use badge,
"Stop camera sharing", the sharing toggle, a floating consent panel and the
screen-lock / session-switch observers. persistentId is keyed with the
store's secret. Consumers receive a READ-ONLY section on every platform
(ipc_shmem_create_with_readonly).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…itch, WTS lock suspension

Icon badge (red dot) + tooltip + menu line "3D camera in use by <app>", one
balloon per transition, "Stop camera sharing", the "Share the 3D camera
with apps" checkmark, a top-most consent dialog (Allow / Allow once / Deny)
hosted on the tray thread for the manager's prompt provider, and
WTSRegisterSessionNotification (lock / console or remote disconnect) plus
SM_REMOTESESSION at start feeding the lock suspension. Written to the API;
needs the Windows box to run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sharing, fake-lock; probe reports events

list/calib/probe connect as CAMERA_CONSUMER (the browser utility's class);
the control ops as DIAG. probe prints every event with its time since
start and exits 4 / 6 / 7 / 8 for consent refused / sharing off / busy /
ended by the service.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Spec §2a + §7 rewritten as built, §8 results, v2 history; roadmap R3 row
and "R3 as built"; env census (DEV_ALLOW as the documented override,
DXR_STEREO_CAMERA_PROMPT); docs/guides/stereo-camera-consent.md for app and
browser developers (incl. the B1 migration); ADR-043 Amendment 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ompt cursor type

Two findings from the R3 hardware run on the SR laptop (#1819):

- 'camera sharing off' then 'camera probe' exited 3 ('the service exposes no
  stereo camera') instead of the documented 6. While sharing is off the service
  enumerates no camera, so the probe never reached the stream-create refusal
  that maps to 6. The probe now asks the service's status (a DIAG-only control,
  so over a second short-lived connection) when the enumeration is empty, and
  reports DISABLED / exit 6 when sharing is off. A machine with no stereo
  camera still exits 3.
- service_tray_win.c: LoadCursorW(NULL, IDC_ARROW) is MSVC C4133 in this
  non-UNICODE build (IDC_ARROW is the ANSI-typed MAKEINTRESOURCE); use
  MAKEINTRESOURCEW(32512).

Compiled on macOS (displayxr-cli, displayxr-service). NOT run: the exit code
needs a service with the sim or SR camera and a verified DIAG CLI; the tray
file is Windows-only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dfattal
dfattal force-pushed the feat/stereo-camera-r3 branch from b567f32 to fa524ce Compare October 6, 2026 07:44
@dfattal
dfattal marked this pull request as ready for review October 6, 2026 07:44
@dfattal
dfattal requested a review from a team as a code owner October 6, 2026 07:44
dfattal added a commit that referenced this pull request Oct 6, 2026
… a stored Deny

Follow-up to PR #1819 (R3 consent). As built, a registered consent-
delegating client was allowed at step 4 of the consent decision, before
the OS camera privacy switch (step 5) and before a stored per-app Deny
(step 6), and tests_camera_consent pinned that order. Once a browser's
installer registered it, it would get the 3D camera with the OS switch
off and a Deny the user had stored silently ignored. Roadmap §B.3 and the
pre-R3 spec say the opposite, and no rationale for the order was recorded;
"the browser enforces the OS switch itself" cannot hold, because the
service, not the browser, opens the camera, so the OS never sees the
browser as a camera user.

New order (spec §7.1 is the one statement of it): sharing off /
DXR_STEREO_CAMERA=0 -> DISABLED; dev override; no identity; OS switch ->
PERMISSION_INSUFFICIENT; stored Deny -> CONSENT_REFUSED; delegating ->
allowed; stored Allow; Allow once; prompt. Delegation keeps meaning
exactly "no runtime prompt and no stored decision needed". The decision's
`delegating` flag is now reported from step 3 on whatever the verdict.

`displayxr-cli camera trust <exe>` over a stored Deny clears the Deny and
says so (the user's newer explicit word, like `allow` over `deny`); an
installer's machine-wide registration never touches the user's store.
`camera deny` on a delegating exe notes that the Deny wins. Explicit <exe>
arguments are stored the way the service sees the peer: resolved with
realpath on POSIX; on Windows read from the UTF-16 command line (argv is
the ANSI code page) and made absolute with GetFullPathNameW.

Windows store reader (u_camera_consent_store.c), both bugs verified:
- it used the ANSI registry API while the runtime compares UTF-8 paths
  (QueryFullProcessImageNameW -> CP_UTF8), so a non-ASCII install path
  could never match an installer's UTF-16 entry. Every read, write, delete
  and enumeration (HKLM and HKCU, Apps, Delegating, Sharing, Secret, the
  ConsentStore webcam keys) now uses the wide API with explicit UTF-8
  conversion.
- fixed 256-char name / 1024-byte data buffers turned one oversized value
  into ERROR_MORE_DATA and `break`, hiding every later entry. Buffers are
  now sized from RegQueryInfoKeyW; an entry that still does not fit, has
  the wrong type or does not convert is skipped, never ends the scan.
The matching is the pure u_camera_consent_list_find() over an abstract
enumerator plus u_camera_consent_path_equal(), both unit-tested on every
OS.

Tests: the delegation-first assertions are rewritten; new cases for
delegating + OS switch off, + stored Deny, + sharing off / kill switch,
+ nothing stored (allowed, no prompt, nothing written), unregistered
neighbour unchanged, path rules, and skip-not-stop enumeration. Mutation:
restoring the old order fails 2 cases / 7 assertions; making the list
scan stop on a skipped entry fails the enumeration case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal
dfattal merged commit 5d167c4 into main Oct 6, 2026
40 checks passed
@dfattal
dfattal deleted the feat/stereo-camera-r3 branch October 6, 2026 08:03
dfattal added a commit that referenced this pull request Oct 6, 2026
… a stored Deny

Follow-up to PR #1819 (R3 consent). As built, a registered consent-
delegating client was allowed at step 4 of the consent decision, before
the OS camera privacy switch (step 5) and before a stored per-app Deny
(step 6), and tests_camera_consent pinned that order. Once a browser's
installer registered it, it would get the 3D camera with the OS switch
off and a Deny the user had stored silently ignored. Roadmap §B.3 and the
pre-R3 spec say the opposite, and no rationale for the order was recorded;
"the browser enforces the OS switch itself" cannot hold, because the
service, not the browser, opens the camera, so the OS never sees the
browser as a camera user.

New order (spec §7.1 is the one statement of it): sharing off /
DXR_STEREO_CAMERA=0 -> DISABLED; dev override; no identity; OS switch ->
PERMISSION_INSUFFICIENT; stored Deny -> CONSENT_REFUSED; delegating ->
allowed; stored Allow; Allow once; prompt. Delegation keeps meaning
exactly "no runtime prompt and no stored decision needed". The decision's
`delegating` flag is now reported from step 3 on whatever the verdict.

`displayxr-cli camera trust <exe>` over a stored Deny clears the Deny and
says so (the user's newer explicit word, like `allow` over `deny`); an
installer's machine-wide registration never touches the user's store.
`camera deny` on a delegating exe notes that the Deny wins. Explicit <exe>
arguments are stored the way the service sees the peer: resolved with
realpath on POSIX; on Windows read from the UTF-16 command line (argv is
the ANSI code page) and made absolute with GetFullPathNameW.

Windows store reader (u_camera_consent_store.c), both bugs verified:
- it used the ANSI registry API while the runtime compares UTF-8 paths
  (QueryFullProcessImageNameW -> CP_UTF8), so a non-ASCII install path
  could never match an installer's UTF-16 entry. Every read, write, delete
  and enumeration (HKLM and HKCU, Apps, Delegating, Sharing, Secret, the
  ConsentStore webcam keys) now uses the wide API with explicit UTF-8
  conversion.
- fixed 256-char name / 1024-byte data buffers turned one oversized value
  into ERROR_MORE_DATA and `break`, hiding every later entry. Buffers are
  now sized from RegQueryInfoKeyW; an entry that still does not fit, has
  the wrong type or does not convert is skipped, never ends the scan.
The matching is the pure u_camera_consent_list_find() over an abstract
enumerator plus u_camera_consent_path_equal(), both unit-tested on every
OS.

Tests: the delegation-first assertions are rewritten; new cases for
delegating + OS switch off, + stored Deny, + sharing off / kill switch,
+ nothing stored (allowed, no prompt, nothing written), unregistered
neighbour unchanged, path rules, and skip-not-stop enumeration. Mutation:
restoring the old order fails 2 cases / 7 assertions; making the list
scan stop on a skipped entry fails the enumeration case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dfattal added a commit that referenced this pull request Oct 6, 2026
… a stored Deny (#1836)

Follow-up to PR #1819 (R3 consent). As built, a registered consent-
delegating client was allowed at step 4 of the consent decision, before
the OS camera privacy switch (step 5) and before a stored per-app Deny
(step 6), and tests_camera_consent pinned that order. Once a browser's
installer registered it, it would get the 3D camera with the OS switch
off and a Deny the user had stored silently ignored. Roadmap §B.3 and the
pre-R3 spec say the opposite, and no rationale for the order was recorded;
"the browser enforces the OS switch itself" cannot hold, because the
service, not the browser, opens the camera, so the OS never sees the
browser as a camera user.

New order (spec §7.1 is the one statement of it): sharing off /
DXR_STEREO_CAMERA=0 -> DISABLED; dev override; no identity; OS switch ->
PERMISSION_INSUFFICIENT; stored Deny -> CONSENT_REFUSED; delegating ->
allowed; stored Allow; Allow once; prompt. Delegation keeps meaning
exactly "no runtime prompt and no stored decision needed". The decision's
`delegating` flag is now reported from step 3 on whatever the verdict.

`displayxr-cli camera trust <exe>` over a stored Deny clears the Deny and
says so (the user's newer explicit word, like `allow` over `deny`); an
installer's machine-wide registration never touches the user's store.
`camera deny` on a delegating exe notes that the Deny wins. Explicit <exe>
arguments are stored the way the service sees the peer: resolved with
realpath on POSIX; on Windows read from the UTF-16 command line (argv is
the ANSI code page) and made absolute with GetFullPathNameW.

Windows store reader (u_camera_consent_store.c), both bugs verified:
- it used the ANSI registry API while the runtime compares UTF-8 paths
  (QueryFullProcessImageNameW -> CP_UTF8), so a non-ASCII install path
  could never match an installer's UTF-16 entry. Every read, write, delete
  and enumeration (HKLM and HKCU, Apps, Delegating, Sharing, Secret, the
  ConsentStore webcam keys) now uses the wide API with explicit UTF-8
  conversion.
- fixed 256-char name / 1024-byte data buffers turned one oversized value
  into ERROR_MORE_DATA and `break`, hiding every later entry. Buffers are
  now sized from RegQueryInfoKeyW; an entry that still does not fit, has
  the wrong type or does not convert is skipped, never ends the scan.
The matching is the pure u_camera_consent_list_find() over an abstract
enumerator plus u_camera_consent_path_equal(), both unit-tested on every
OS.

Tests: the delegation-first assertions are rewritten; new cases for
delegating + OS switch off, + stored Deny, + sharing off / kill switch,
+ nothing stored (allowed, no prompt, nothing written), unregistered
neighbour unchanged, path rules, and skip-not-stop enumeration. Mutation:
restoring the old order fails 2 cases / 7 assertions; making the list
scan stop on a skipped entry fails the enumeration case.

Co-authored-by: Claude Opus 5.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