diff --git a/CLAUDE.md b/CLAUDE.md index 47b62d16a3..3f952f990e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ Vendor display drivers ship as **plug-in DLLs** from their own repos (ADR-019). - `XR_DXR_android_surface_binding` — app passes its own Android Surface/`ANativeWindow`; `xrSetAndroidSurfaceDXR` republishes it across background/resume and `xrSetAndroidWindowGeometryDXR` feeds the per-frame window rect (ADR-036 D6). **Required for multi-window on Android** — the runtime-spawned SurfaceView is `_hosted`-fullscreen-only - `XR_DXR_mcp_tools` — app registers its own MCP tools (agent control surface); event-queue dispatch via `XrEventDataMCPToolCallDXR` - `XR_DXR_depth_budget` — advisory **rear depth budget**: how far behind the display plane a transparent app may render (`farOffsetVH`, 0 = clip at the ZDP, 1000 = unrestricted), chained on `XrViewState` at `xrLocateViews`. The runtime owns the policy (it measures the background's horizontal-disparity cue), the DP owns pixels, the app owns geometry — ADR-040 -- `XR_DXR_lift` — 2D→3D **conversion service**: a vendor plug-in's module (depth / SBS / N-view / photo→splats) exposed generically as async latest-wins streams (IPC-only, D3D11 service lift thread); SBS/N-view results are woven on the ordinary weave path (a weave rect can be flagged "lift me"), and a READY module supersedes the browser/SDK open default — ADR-042 +- `XR_DXR_lift` — 2D→3D **conversion service**: a vendor plug-in's module (depth / SBS / N-view / photo→splats) exposed generically as async latest-wins streams (conversion always in the D3D11 service's lift thread — IPC sessions use their own connection, in-process D3D11/D3D12 sessions such as the Unity provider a lift-only connection, ADR-049); SBS/N-view results are woven on the ordinary weave path (a weave rect can be flagged "lift me"), and a READY module supersedes the browser/SDK open default — ADR-042 The list above is highlights, not the catalog — **`docs/specs/extensions/index.json` is the catalog**: one hand-written note (group, title, one-line summary) per published extension, joined to the `XR_DXR_*.h` headers by `scripts/gen_extensions_index.py`. It generates the `displayxr-extensions` mirror's `README.md` + machine-readable `extensions.json`, and `displayxr-website` merges its longer editorial prose onto that by name — so adding a header is all it takes for an extension to appear on every public surface. `lint.yml` runs `--check` on every PR, so a header with no note (or a note with no header) fails the build. That guard exists because the mirror's README was a frozen heredoc that documented 5 of 16 extensions for months (displayxr-extensions#2). diff --git a/docs/README.md b/docs/README.md index c591be08e0..da36c3c05f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -156,6 +156,7 @@ Integrate your 3D display hardware into DisplayXR. - [ADR-046](adr/ADR-046-depth-aware-cursor.md) — Depth-aware cursor — opt-in only; the app knows the depth, the runtime places the cursor - [ADR-047](adr/ADR-047-multi-screen-segments-and-per-screen-views.md) — Multi-screen — segments and per-screen views - [ADR-048](adr/ADR-048-lift-viewpoint-policy.md) — Lift viewpoint policy — window-relative viewpoints, rig gains, axis mode and ease-back are runtime policy +- [ADR-049](adr/ADR-049-in-process-lift-rides-the-service.md) — In-process lift rides the service — one conversion module per machine, reached over a lift-only connection --- diff --git a/docs/adr/ADR-049-in-process-lift-rides-the-service.md b/docs/adr/ADR-049-in-process-lift-rides-the-service.md new file mode 100644 index 0000000000..7eecff77dd --- /dev/null +++ b/docs/adr/ADR-049-in-process-lift-rides-the-service.md @@ -0,0 +1,196 @@ +# ADR-049: In-process lift rides the service — one conversion module per machine, reached over a lift-only connection + +**Status:** Proposed (2026-10-08) · Phase 1 implemented (runtime routing) · extends +[ADR-042](ADR-042-vendor-2d3d-conversion-supersedes-default.md) · +spec: [XR_DXR_lift.md](../specs/extensions/XR_DXR_lift.md) §2, §5.3, §10 · issue #1862, epic #1858 + +## In one paragraph + +`XR_DXR_lift` (ADR-042) converts 2D content to 3D with the vendor's conversion module, which +runs inside `displayxr-service.exe` on a lift thread with its own D3D11 device. An in-process +session — every engine app, and above all the Unity display provider, which renders in-process +on Unity's own D3D12 device — got `XR_ERROR_FEATURE_UNSUPPORTED` from every lift call, so no +engine app could use the module. This ADR keeps the module where it is and gives an in-process +D3D11 / D3D12 session a **second, sessionless, lift-only connection to the service**: the same +IPC calls the browser and `displayxr-cli lift` already make, results back as the same NT handles +(a texture and a shared fence) that a D3D12 device opens with `OpenSharedHandle`. Rendering +stays in-process; conversion stays in the one service. Nothing in the plug-in contract changes. + +## Context + +- **Streams belong to a connection, not a session.** That is already how the service implements + lift (ADR-042, spec §9): `displayxr-cli lift` drives it headless over a sessionless DIAG + connection, and the service admits `APP` connections to every lift call. +- **The export is already D3D12-consumable.** A result's export texture is + `SHARED | SHARED_NTHANDLE` with **no keyed mutex**, and its fence is an `ID3D11Fence` created + `D3D11_FENCE_FLAG_SHARED`. `ID3D12Device::OpenSharedHandle` opens both, as an + `ID3D12Resource` and an `ID3D12Fence`; the app's queue `Wait`s the fence value. No new export + kind is needed. +- **The vendor conversion runtime is process-global and single-worker.** The first module behind + the lift slots keeps its properties (model, output type, tuning) in process-wide state and + marshals every call to one worker thread. A second process that loads it gets a second copy + of the model in VRAM (hundreds of MB to GB), a second warm-up (licence check, engine build: + seconds), and a second worker competing for the same GPU with no shared scheduler. Two + converters per machine is the expensive shape this decision is mostly about. +- **Scheduling is runtime policy** (ADR-042 §4): priorities and round-robin across every + stream on the machine only work if every stream is in the scheduler. +- **Hybrid boxes.** On an iGPU + dGPU machine the service's ingest device — where lift + textures live — is on the adapter it publishes as `client_d3d_deviceLUID` (ADR-037 §7). A + shared NT handle opens only on a device of the **same adapter**. + +## Options + +### (A) Shared lift module, instantiated in the app process + +Refactor `d3d11_lift.cpp` into a module the in-process runtime can also host: its own D3D11 +device in the app process, a lift-only DP from the in-process instance's plug-in +(`create_dp_d3d11_lift`, which the in-process instance already discovers), results shared to +the app's D3D12 device through NT handles and a shared fence. + +- **Vendor neutrality:** fine — the same factory and slots as the service. +- **Latency:** best case one IPC round trip less per call (~0.1 ms) — immaterial next to a + conversion of tens of ms. +- **Adapter:** the lift device can be created on the app's own adapter, so no cross-adapter + hazard. This is A's one real advantage. +- **ABI:** none. +- **Vendor-runtime state:** **the deciding cost.** Every engine app that lifts loads its own + copy of the vendor runtime: double VRAM and double warm-up whenever a browser (service-side + module) and an engine app run together, and two workers fighting over one GPU with no common + scheduler. Process-global properties also mean two streams in one app process can still + stomp each other's settings — the service already serialises that; A would have to again. +- **Cost:** large. The module is wired to service-only pieces (the service immediate-context + mutex, the weave-rect pin path, the service device). Splitting it is a refactor of a + 2,200-line file that the shipping browser path depends on. + +### (B) A native D3D12 lift slot in the vendor contract + +Append `lift_*` slots to `xrt_display_processor_d3d12` (or a `create_dp_d3d12_lift` factory) +so a plug-in converts directly on a D3D12 device. + +- **Vendor neutrality:** fine in form, but it doubles what every vendor must implement for one + feature (a D3D11 and a D3D12 path), and the first module's runtime is D3D11 / DirectML + internally — the plug-in would bridge back to D3D11 anyway. +- **Latency:** no better than A. +- **ABI:** append-only slots under ADR-020 — allowed, no ABI bump, but permanent surface. +- **Vendor-runtime state:** the same per-process duplication as A, since the slot still runs + in the app process. +- **Cost:** vendor work in every plug-in plus everything A needs in the runtime. + +### (C) In-process sessions reach the service's lift over a lift-only IPC connection (hybrid) + +The app renders in-process exactly as today. Its lift calls open — lazily, in the background — +a sessionless `APP` connection to the service and run the existing lift IPC calls on it. +Results come back as the existing NT handles. + +- **Vendor neutrality:** nothing vendor-specific anywhere; no plug-in change. +- **Latency:** each lift call is one IPC round trip (submit: round trip + one blit in the + service, the same as the browser pays). Conversion itself is unchanged and dominates. + Never on the app's frame path for longer than the round trip: the connect is asynchronous, + and only `xrCreateLiftStreamDXR` (a setup call) waits for it. +- **Adapter:** results live on the service's ingest adapter. An app on another adapter cannot + open them — the same constraint every IPC D3D app already has, and the one ADR-037 §7 + already steers apps around with `xrGetD3D12GraphicsRequirementsKHR`. An engine that picks + its own adapter (Unity) must check `OpenSharedHandle` and treat a failure as + `UNAVAILABLE`; a follow-up exposes the lift adapter LUID in properties (§Follow-ups). +- **ABI:** none (no plug-in change, no wire change, no header change). +- **Vendor-runtime state:** **one module per machine.** The vendor runtime is loaded once, by + the service, warmed once, and every stream on the box — browser tabs, calls, engine apps — + shares its VRAM and one scheduler with priorities that mean something across apps. +- **Cost:** small and additive: a connection holder in `ipc_client` and a router in + `oxr_lift.c`. The browser / CLI / IPC-session path is unchanged. +- **Costs specific to C:** the app depends on a running service for lift (it already does for + the weave on the browser path; an engine app without a service falls back exactly as it + does on a machine without a module), and the connection is subject to the client↔service + version-tag gate (a from-source runtime against an installed service reads `UNAVAILABLE`). + +### Scorecard + +| | (A) in-process module | (B) D3D12 slot | (C) lift over IPC | +|---|---|---|---| +| Vendor runtime instances per machine | one per lifting process | one per lifting process | **one** | +| VRAM / warm-up | doubled with a browser running | doubled | **shared** | +| Cross-app scheduling (ADR-042 §4) | per process | per process | **machine-wide** | +| Plug-in ABI | none | appended slots | **none** | +| Vendor work | none | a D3D12 path each | **none** | +| Latency per call | lowest | lowest | + one IPC round trip (~0.1 ms) | +| Hybrid adapters | app's adapter | app's adapter | service ingest adapter (app must match) | +| Runtime change | large refactor | large + vendor | **small, additive** | + +## Decision + +**(C).** An in-process D3D11 or D3D12 session on Windows reaches the service's conversion +module over a lift-only connection it owns. One vendor conversion runtime per machine is worth +far more than the sub-millisecond IPC hop A and B save, and C needs nothing from vendors. + +1. **The connection.** `ipc_client_lift_link` (in `ipc_client`) owns one sessionless + connection per in-process session, declared `APP` and named ` (in-process lift)`. + It is created on the session's first lift call and connects on a background thread; it + never launches the service (it dials only when the service's single-instance mutex + exists). A failed connect is retried at most every 5 s. It is closed when the session is + destroyed, after the session's lift streams; closing it drops the streams service-side, + as for any connection. +2. **Routing.** `oxr_lift.c` picks the connection per call: an IPC session's own, or the + in-process session's link. Every call then runs the same `ipc_client_lift_*` code. Other + in-process sessions (GL, Vulkan, Metal) keep `XR_ERROR_FEATURE_UNSUPPORTED`. +3. **States while not connected.** `xrGetLiftPropertiesDXR` reports `ACTIVATING` while the + connect is in flight and `UNAVAILABLE` (`supportedModes = 0`) when there is no service — + the consumer contract of ADR-042 then does the right thing unmodified. + `xrCreateLiftStreamDXR` waits up to 6 s for an in-flight connect (setup call), then + returns `XR_ERROR_RUNTIME_FAILURE` (still connecting, retry) or + `XR_ERROR_FEATURE_UNSUPPORTED` (no service). +4. **Connection loss never loses the session.** The session renders in-process, so a dead + lift connection (service restart) is a transient `XR_ERROR_RUNTIME_FAILURE` on the call + that sees it; the stream handles of that connection then return + `XR_ERROR_FEATURE_UNSUPPORTED` ("recreate the stream"), and the next properties query + reconnects. (An IPC session keeps the weave §4b contract: `XR_ERROR_INSTANCE_LOST`.) +5. **Result consumption on D3D12.** The app opens `outputTexture` and `fence` with + `ID3D12Device::OpenSharedHandle`, closes the handles, `ID3D12CommandQueue::Wait`s + `fenceValue` before sampling, and finishes sampling before its next acquire on the stream + (spec §5.1, unchanged). +6. **Input on D3D12.** The input handle is a D3D12 shared resource + (`D3D12_HEAP_FLAG_SHARED`, `CreateSharedHandle`) — no keyed mutex, which D3D12 cannot + take. The service snapshots it at submit with no GPU wait, so the app makes its writes + complete before `xrSubmitLiftFrameDXR` (fence + CPU wait, or the frame's existing + completion) and does not rewrite the texture until a later frame — double-buffering the + input is the simple way. A submit-side fence is a follow-up (§Follow-ups). + +## Consequences + +- **+** Engine apps get the vendor module with no plug-in change, no ABI change, no wire or + header change; the existing ADR-042 consumer contract applies verbatim. +- **+** One vendor conversion runtime on the machine: shared VRAM, one warm-up, one scheduler + across the browser, calls and engine apps. +- **+** The IPC-session path is untouched (its calls now go through the same + `ipc_client_lift_*` functions directly instead of one-line bridges). +- **−** Lift for an in-process app needs the service running and version-matched. +- **−** An extra sessionless `APP` connection per lifting in-process session; it counts + against the service's APP quota and shows in `displayxr-cli clients`. +- **−** Results live on the service's ingest adapter; an engine on a different adapter cannot + open them (it sees `OpenSharedHandle` fail and must fall back). +- **−** No GPU-side input sync for D3D12 callers yet (decision 6). + +## Follow-ups + +- **Lift adapter in properties.** Chain an `XrLiftAdapterPropertiesDXR { LUID adapterLuid }` + on `XrLiftPropertiesDXR` so an engine that picked its own adapter knows *before* opening a + handle whether lift results are reachable. Spec bump; coordinate with the lift viewpoint + policy bump (ADR-048). +- **Submit-side fence.** Chain an `XrLiftSubmitSyncDXR { fence handle, waitValue }` on + `XrLiftFrameSubmitInfoDXR`; the service `Wait`s it on its context before the snapshot blit. + Removes decision 6's CPU wait / double-buffer requirement for D3D12 callers. +- **Weave rects stay IPC-only.** `XrWeaveSubmitLiftRectsDXR` belongs to the weave path, which + in-process sessions do not use; an in-process engine app composes the SBS / N-view result + into its own views. +- **Unity provider** (`displayxr-unity`, separate PR): consume lift from the D3D12 provider — + properties gate, stream lifecycle, D3D12 input double-buffer, `OpenSharedHandle` of the + export + `Wait` on the fence, a C# surface for scripts (e.g. a camera-feed component). + +## Alternatives rejected + +- **(A) and (B)** — above: per-process vendor runtime instances are the cost that matters, and + B adds vendor surface for no gain over A. +- **Force engine apps onto the IPC path** (`XRT_FORCE_MODE=ipc`) when they enable + `XR_DXR_lift`. Unity under the service takes the client → IPC → D3D11-service route, which + costs the provider its zero-copy D3D12 swapchain and the in-process compositor it is built + around; lift should not decide how an app renders. +- **Reuse the session's compositor connection.** An in-process session has none. diff --git a/docs/adr/README.md b/docs/adr/README.md index 89c4499652..2f2ec2217d 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -52,3 +52,4 @@ - [ADR-046](ADR-046-depth-aware-cursor.md) — Depth-aware cursor — opt-in only; the app knows the depth, the runtime places the cursor - [ADR-047](ADR-047-multi-screen-segments-and-per-screen-views.md) — Multi-screen — segments and per-screen views - [ADR-048](ADR-048-lift-viewpoint-policy.md) — Lift viewpoint policy — window-relative viewpoints, rig gains, axis mode and ease-back are runtime policy +- [ADR-049](ADR-049-in-process-lift-rides-the-service.md) — In-process lift rides the service — one conversion module per machine, reached over a lift-only connection diff --git a/docs/architecture/unity-d3d12-app-path.md b/docs/architecture/unity-d3d12-app-path.md index 9fc8ce9d20..191880aa03 100644 --- a/docs/architecture/unity-d3d12-app-path.md +++ b/docs/architecture/unity-d3d12-app-path.md @@ -99,6 +99,16 @@ the VK avatar demo. Two consequences worth knowing on the runtime side: - The zones path softens the old "fixed-resolution" caveat: per-**zone** resolution follows the zone rect via live realloc, even though per-**eye** stereo topology stays fixed at 2. +## 2D→3D conversion (`XR_DXR_lift`) from the in-process provider + +The provider's session is in-process, yet it can use the vendor conversion module: the +runtime opens a sessionless, lift-only connection to the service on the first lift call and +runs the ordinary lift calls on it (ADR-049) — one conversion runtime per machine, shared with +the browser. Results come back as NT handles the provider opens on Unity's D3D12 device +(`OpenSharedHandle` of the texture and fence, `Wait` on the fence); inputs are D3D12 shared +textures without a keyed mutex, so the provider completes and double-buffers them (spec §5.3). +Lifted weave rects stay IPC-only — the provider composes SBS/N-view results into its own eyes. + ## How this differs from a native D3D12 app (`cube_handle_d3d12_win`) | | `cube_handle_d3d12_win` | Unity D3D12 (provider) | diff --git a/docs/specs/extensions/XR_DXR_lift.md b/docs/specs/extensions/XR_DXR_lift.md index f1951b6374..f5e93aaa39 100644 --- a/docs/specs/extensions/XR_DXR_lift.md +++ b/docs/specs/extensions/XR_DXR_lift.md @@ -4,10 +4,10 @@ |---|---| | **Extension Name** | `XR_DXR_lift` | | **Spec Version** | 3 | -| **Extension Type** | Instance extension (service path — Windows/D3D11; advertised on every desktop platform, where a service without a module reports `supportedModes = 0`) | +| **Extension Type** | Instance extension (conversion in the service — Windows/D3D11; IPC sessions, and in-process D3D11/D3D12 sessions on Windows over a lift-only connection (ADR-049); advertised on every desktop platform, where a service without a module reports `supportedModes = 0`) | | **Header** | `src/external/openxr_includes/openxr/XR_DXR_lift.h` (canonical; auto-syncs to `displayxr-extensions`) | | **Status** | Provisional (`1004999270–285` block, pending Khronos registry) | -| **Decision record** | [ADR-042](../../adr/ADR-042-vendor-2d3d-conversion-supersedes-default.md); viewpoint policy, app rig and auxiliary depth: [ADR-048](../../adr/ADR-048-lift-viewpoint-policy.md) (+ Addendum A) | +| **Decision record** | [ADR-042](../../adr/ADR-042-vendor-2d3d-conversion-supersedes-default.md); viewpoint policy, app rig and auxiliary depth: [ADR-048](../../adr/ADR-048-lift-viewpoint-policy.md) (+ Addendum A) · in-process: [ADR-049](../../adr/ADR-049-in-process-lift-rides-the-service.md) | | **Plug-in contract** | `src/xrt/include/xrt/xrt_dp_lift.h` + the lift slots of `xrt_display_processor_d3d11` (`XRT_DP_D3D11_HAS_LIFT`), [`xrt_plugin_iface.md` § lift](../../reference/xrt_plugin_iface.md#turning-2d-into-3d-the-lift-slots-adr-042-xr_dxr_lift) | ## 1. What it is @@ -51,7 +51,8 @@ if (props.state == XR_LIFT_STATE_READY_DXR && (props.supportedModes & XR_LIFT_MO | Situation | `xrGetLiftPropertiesDXR` | |---|---| -| In-process session (any platform) | `XR_ERROR_FEATURE_UNSUPPORTED` — lift is IPC-only, like the weave service | +| In-process D3D11 / D3D12 session on Windows (engine apps, e.g. the Unity display provider) | the service's module, reached over a lift-only connection the session opens in the background on its first lift call (ADR-049): `ACTIVATING` while it connects, then the rows below; `UNAVAILABLE` when no service is running (it is never launched for this) or the service refuses the connection (e.g. a from-source runtime against a different installed service) | +| Any other in-process session (GL, Vulkan, Metal; any non-Windows platform) | `XR_ERROR_FEATURE_UNSUPPORTED` | | Service without a module (macOS, Linux, Android, or a Windows plug-in without lift slots, sim_display) | `XR_SUCCESS`, `supportedModes = 0`, `UNAVAILABLE` | | First query on a Windows service with a module | `ACTIVATING` (the service is bringing its lift device + the plug-in's lift DP up in the background) | | Module loading (model weights, licence check, engine build) | `ACTIVATING` — poll ≤ 2 Hz, do not fall back permanently | @@ -331,6 +332,23 @@ Why a separate entry point rather than a struct chained on the texture acquire: kinds never coexist on a stream, and a blob needs the two-call size negotiation a texture does not. One call per result kind keeps each contract minimal. +### 5.3 D3D12 callers (engine apps, ADR-049) + +The handles are D3D12-consumable as they are: the export texture is an NT-handle shared texture +**without** a keyed mutex, the fence an NT-handle shared fence. + +- **Results:** `ID3D12Device::OpenSharedHandle(outputTexture)` → `ID3D12Resource`, + `OpenSharedHandle(fence)` → `ID3D12Fence`; close both handles. Per result: + `ID3D12CommandQueue::Wait(fence, fenceValue)` before the first read. The rest of §5.1 holds. +- **Adapter:** results live on the service's ingest adapter (the LUID an IPC D3D session is + told to use). A device on another adapter cannot open them — treat an `OpenSharedHandle` + failure as `UNAVAILABLE` and fall back. +- **Input:** create the input with `D3D12_HEAP_FLAG_SHARED` and `ID3D12Device::CreateSharedHandle` + and pass that NT handle (`inputIsDxgi = XR_FALSE`). D3D12 has no keyed mutex, so the service + snapshots the texture at submit **without a GPU wait**: complete the writes before + `xrSubmitLiftFrameDXR` (signal + CPU-wait a fence) and do not rewrite the texture until a later + frame — double-buffer the input. A submit-side fence is planned (ADR-049 follow-ups). + ## 6. Lifting weave rects ```c @@ -465,7 +483,12 @@ UI rates. - **Vendor knobs live in the SERVICE's environment** (e.g. a module's backend selection), not the app's — the conversion runs in `displayxr-service.exe`. - **Streams belong to the IPC connection**, not the session, and die with it; the calls need no - compositor session, which is what lets `displayxr-cli lift` drive them headless. + compositor session, which is what lets `displayxr-cli lift` drive them headless — and what lets + an in-process session use lift (ADR-049): it opens a sessionless `APP` connection named + ` (in-process lift)` (`ipc_client_lift_link.c`, background connect, never launches the + service, retried at most every 5 s) and runs the same calls on it. WARN lines in the app's + log: `[lift] in-process: lift-only service connection up (generation N)`, `... no service + connection for 2D->3D conversion ...` (once), `... connection (generation N) is gone ...`. ## 10. Error codes and connection loss @@ -475,11 +498,11 @@ Mirrors XR_DXR_weave §4b. |---|---|---| | `XR_LIFT_NOT_READY_DXR` (success) | no result newer than the last acquired | yes | | `XR_ERROR_VALIDATION_FAILURE` | a struct out of contract (mode not one bit, bad viewCount, EXPLICIT without viewpoints, `XrLiftDepthRequestDXR` on a DEPTH / GAUSSIANS stream, wrong acquire for the mode, bad `rectIndex`) | yes — caller bug, nothing sent | -| `XR_ERROR_FEATURE_UNSUPPORTED` | in-process session; mode not supported by a READY module; no module | yes — permanent for that request | +| `XR_ERROR_FEATURE_UNSUPPORTED` | in-process session other than D3D11/D3D12 on Windows; mode not supported by a READY module; no module; in-process: a stream of a lift connection that was lost (destroy it, create a new one) | yes — permanent for that request | | `XR_ERROR_LIMIT_REACHED` | `maxStreams` reached | yes | | `XR_ERROR_SIZE_INSUFFICIENT` | blob capacity too small (latch kept) | yes | | `XR_ERROR_RUNTIME_FAILURE` | the service refused this call over a healthy pipe — most commonly the 4 ms input keyed-mutex miss, or a stream the service no longer knows | **yes — retry next frame** | -| `XR_ERROR_INSTANCE_LOST` | the IPC connection is gone | no — recover with a new instance (weave §4b) | +| `XR_ERROR_INSTANCE_LOST` | the IPC connection is gone (IPC sessions only — an in-process session's lift connection dying is `XR_ERROR_RUNTIME_FAILURE` on that call; the session renders on, and the next `xrGetLiftPropertiesDXR` reconnects) | no — recover with a new instance (weave §4b) | | `XR_ERROR_SESSION_LOST` | any call after that | no | ## 11. Vendor contract, in one table @@ -531,6 +554,7 @@ WARP + the lift-only factory): modes 0 passes; only malformed caps fail | DisplayXR Browser (Chromium fork) | GPU process → service | `xrGetLiftPropertiesDXR` to decide vendor vs open default (ADR-042); `XrWeaveSubmitLiftRectsDXR` for inline 2D video/images lifted in place; `xrSubmitLiftFrameDXR` + acquire where the page wants the depth/views itself | | DisplayXR web SDK (`@displayxr/inline3d`) | via the browser | the same policy: prefer the runtime module when READY; GAUSSIANS via the blob path supersedes the SDK's open photo → splats lift | | 3D calling (up to 4 mono tiles) | app → service | one SBS stream per tile; `xrSetLiftStreamPriorityDXR` HIGH for the active speaker | +| Engine apps (Unity display provider, D3D12 in-process) | in-process session → lift-only connection → service (ADR-049) | streams + texture acquire (§5.3); no weave rects (the app composes the views itself) | | `displayxr-cli lift` | DIAG IPC | caps + the N0 probe (§12) | When changing the header, byte-sync every consumer's vendored copy and rebuild it — coupled-PR @@ -544,6 +568,10 @@ order: runtime → extensions auto-sync → consumers. | 2 | Viewpoint policy (ADR-048, §4.1): viewpoints relative to the lifted rect / submitting window centre (was: the panel centre), `XrLiftViewControlDXR` (ipd / parallax factors, axis mode, offset clamp, recentering; defaults X + EASE_BACK 1 s / 2 s), `XrLiftResultViewpointsDXR` echo. | | 3 | App camera + metric depth (ADR-048 Addendum A, §4.2–4.3): `XrDisplayRigDXR` / `XrCameraRigDXR` on `XrLiftOptionsDXR` / `XrWeaveRectLiftDXR` drive the TRACKED viewpoints with the `xrLocateViews` rig math; EXPLICIT viewpoints accepted on lifted weave rects; auxiliary depth for SBS / NVIEW streams — `XrLiftDepthRequestDXR`, `XrLiftDepthResultDXR` (texture + shared fence, units, encoding, intrinsics, convergence depth, viewpoint, `depthToDisplay`), `XrLiftDepthPropertiesDXR`. A separate version from 2 because v2 can ship on its own (#1864): an app tests `SPEC_VERSION >= 3` for these. | +Runtime behaviour, no spec-version change: in-process D3D11 / D3D12 sessions on Windows use lift +over a lift-only connection (ADR-049). A consumer detects it the same way as before — an older +runtime answers `xrGetLiftPropertiesDXR` on such a session with `XR_ERROR_FEATURE_UNSUPPORTED`. + ## Probing on a Windows box — gotchas (first N0 run, 2026-09-25) - **Selecting the display processor.** `XRT_PREFERRED_PLUGIN_ID` does *not* switch the active DP when an installed vendor plug-in also probes; `DXR_PLUGIN_EXCLUSIVE=` in the **service's** environment does (relaunch the service non-elevated from a `.bat` that sets it). `SIM_DISPLAY_FAKE_LIFT=1` alongside `DXR_PLUGIN_EXCLUSIVE=sim-display` exercises the whole path hardware-free (caps modes = DEPTH|SBS|NVIEW|GAUSSIANS). diff --git a/src/xrt/ipc/CMakeLists.txt b/src/xrt/ipc/CMakeLists.txt index 845865f079..a1d9d0b76a 100644 --- a/src/xrt/ipc/CMakeLists.txt +++ b/src/xrt/ipc/CMakeLists.txt @@ -70,6 +70,8 @@ add_library( client/ipc_client_connection.c client/ipc_client_lift.c client/ipc_client_lift.h + client/ipc_client_lift_link.c + client/ipc_client_lift_link.h client/ipc_client_stereo_camera.c client/ipc_client_stereo_camera.h client/ipc_client_device.c diff --git a/src/xrt/ipc/client/ipc_client_compositor.c b/src/xrt/ipc/client/ipc_client_compositor.c index 87de843a68..f6a20fdb88 100644 --- a/src/xrt/ipc/client/ipc_client_compositor.c +++ b/src/xrt/ipc/client/ipc_client_compositor.c @@ -3183,8 +3183,9 @@ ipc_client_create_system_compositor(struct ipc_connection *ipc_c, /* * XR_DXR_lift bridges (ADR-042) — thin accessors the OpenXR state tracker - * (oxr_lift.c) forward-declares, like the weave bridges above: extract the - * session's connection and forward to ipc_client_lift.c. Streams belong to the + * (oxr_lift.c, oxr_weave.c) forward-declares, like the weave bridges above: + * extract the session's connection (and, for weave rects, forward to + * ipc_client_lift.c). Streams belong to the * connection, so a session's streams die with its connection. */ static struct ipc_connection * @@ -3197,97 +3198,15 @@ lift_conn(struct xrt_compositor *xc) return icc != NULL ? icc->ipc_c : NULL; } -xrt_result_t -comp_ipc_client_compositor_lift_get_properties(struct xrt_compositor *xc, struct xrt_dp_lift_caps *out_caps) -{ - return ipc_client_lift_get_properties(lift_conn(xc), out_caps); -} - -xrt_result_t -comp_ipc_client_compositor_lift_stream_create(struct xrt_compositor *xc, - uint32_t mode, - uint32_t content_hint, - float input_scale, - uint32_t aux_outputs, - uint32_t aux_depth_format, - uint64_t *out_stream_id) -{ - return ipc_client_lift_stream_create(lift_conn(xc), mode, content_hint, input_scale, aux_outputs, - aux_depth_format, out_stream_id); -} - -xrt_result_t -comp_ipc_client_compositor_lift_stream_destroy(struct xrt_compositor *xc, uint64_t stream_id) -{ - return ipc_client_lift_stream_destroy(lift_conn(xc), stream_id); -} - -xrt_result_t -comp_ipc_client_compositor_lift_submit(struct xrt_compositor *xc, - uint64_t stream_id, - xrt_graphics_buffer_handle_t handle, - bool is_dxgi, - uint32_t width, - uint32_t height, - int64_t source_time, - const struct xrt_dp_lift_params *params, - const float *viewpoints, - uint32_t viewpoint_count, - const struct xrt_lift_view_control *view_control, - const struct xrt_lift_rig *rig, - uint64_t *out_frame_id) -{ - return ipc_client_lift_submit(lift_conn(xc), stream_id, handle, is_dxgi, width, height, source_time, params, - viewpoints, viewpoint_count, view_control, rig, out_frame_id); -} - -xrt_result_t -comp_ipc_client_compositor_lift_acquire(struct xrt_compositor *xc, - uint64_t stream_id, - bool *out_ready, - struct xrt_lift_result *out_result) -{ - return ipc_client_lift_acquire(lift_conn(xc), stream_id, out_ready, out_result); -} - -xrt_result_t -comp_ipc_client_compositor_lift_get_output(struct xrt_compositor *xc, - uint64_t stream_id, - bool *out_have, - xrt_graphics_buffer_handle_t *out_handle) -{ - return ipc_client_lift_get_output(lift_conn(xc), stream_id, out_have, NULL, NULL, NULL, out_handle); -} - -xrt_result_t -comp_ipc_client_compositor_lift_get_depth_output(struct xrt_compositor *xc, - uint64_t stream_id, - bool *out_have, - xrt_graphics_buffer_handle_t *out_handle) -{ - return ipc_client_lift_get_depth_output(lift_conn(xc), stream_id, out_have, NULL, NULL, NULL, out_handle); -} - -xrt_result_t -comp_ipc_client_compositor_lift_get_fence(struct xrt_compositor *xc, - uint64_t stream_id, - bool *out_have, - xrt_graphics_sync_handle_t *out_handle) -{ - return ipc_client_lift_get_fence(lift_conn(xc), stream_id, out_have, out_handle); -} - -xrt_result_t -comp_ipc_client_compositor_lift_acquire_blob(struct xrt_compositor *xc, - uint64_t stream_id, - uint64_t capacity, - uint8_t *out_bytes, - bool *out_ready, - bool *out_delivered, - struct xrt_lift_blob_info *out_info) +/*! + * The session's connection, for oxr_lift.c: every stream call goes straight to + * ipc_client_lift.c on it — the same calls an in-process session makes on its + * lift-only connection (ADR-049), so one code path serves both. + */ +struct ipc_connection * +comp_ipc_client_compositor_lift_connection(struct xrt_compositor *xc) { - return ipc_client_lift_acquire_blob(lift_conn(xc), stream_id, capacity, out_bytes, out_ready, out_delivered, - out_info); + return lift_conn(xc); } xrt_result_t @@ -3297,17 +3216,3 @@ comp_ipc_client_compositor_lift_weave_rects(struct xrt_compositor *xc, { return ipc_client_lift_weave_rects(lift_conn(xc), count, rects); } - -xrt_result_t -comp_ipc_client_compositor_lift_set_priority(struct xrt_compositor *xc, uint64_t stream_id, uint32_t priority) -{ - return ipc_client_lift_set_priority(lift_conn(xc), stream_id, priority); -} - -xrt_result_t -comp_ipc_client_compositor_lift_stats(struct xrt_compositor *xc, - uint64_t stream_id, - struct xrt_lift_stream_stats *out_stats) -{ - return ipc_client_lift_stats(lift_conn(xc), stream_id, out_stats); -} diff --git a/src/xrt/ipc/client/ipc_client_lift_link.c b/src/xrt/ipc/client/ipc_client_lift_link.c new file mode 100644 index 0000000000..8bc7a69515 --- /dev/null +++ b/src/xrt/ipc/client/ipc_client_lift_link.c @@ -0,0 +1,289 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Lift-only service connection for in-process sessions (see + * ipc_client_lift_link.h, ADR-049). + * @ingroup ipc_client + */ + +#include "xrt/xrt_config_os.h" +#include "xrt/xrt_instance.h" + +#include "os/os_threading.h" +#include "os/os_time.h" + +#include "util/u_logging.h" +#include "util/u_misc.h" + +#include "client/ipc_client.h" +#include "client/ipc_client_connection.h" +#include "client/ipc_client_lift_link.h" + +#ifdef XRT_OS_WINDOWS +#include +#endif + +#include +#include +#include + +//! A failed connect is retried no more often than this. +#define LIFT_LINK_RETRY_NS (5ull * 1000 * 1000 * 1000) + +struct ipc_client_lift_link +{ + struct os_mutex mtx; + enum ipc_client_lift_link_state state; + + //! The current connection (CONNECTED), or the dead one awaiting its last + //! release (DEAD). Owned. + struct ipc_connection *conn; + //! Holders of @ref conn (acquire without a release yet). + uint32_t users; + //! Bumped on every new connection. + uint32_t gen; + + struct os_thread thread; + bool thread_needs_join; + //! The connect thread has finished (its result is in state/conn). + bool connect_done; + uint64_t retry_after_ns; + + //! One WARN per link for "no service", not one per retry. + bool warned_unavailable; + + char label[XRT_MAX_APPLICATION_NAME_SIZE]; +}; + +/*! + * Is a service process alive in this session? The lift link must never LAUNCH + * the service as a side effect of an app asking whether a conversion module + * exists (the ordinary connect falls back to launching it), so it only dials + * when the service's single-instance mutex can be opened. + */ +static bool +service_alive(void) +{ +#ifdef XRT_OS_WINDOWS + HANDLE m = OpenMutexW(SYNCHRONIZE, FALSE, L"Local\\DisplayXR.Service.Singleton"); + if (m == NULL) { + return false; + } + CloseHandle(m); + return true; +#else + return true; // POSIX: no auto-launch on the plain socket connect. +#endif +} + +static void * +connect_run(void *ptr) +{ + struct ipc_client_lift_link *link = (struct ipc_client_lift_link *)ptr; + + struct ipc_connection *c = NULL; + xrt_result_t xret = XRT_ERROR_IPC_FAILURE; + if (service_alive()) { + c = U_TYPED_CALLOC(struct ipc_connection); + if (c != NULL) { + struct xrt_instance_info ii; + U_ZERO(&ii); + snprintf(ii.app_info.application_name, sizeof(ii.app_info.application_name), "%s", link->label); + // An ordinary APP connection that never creates a session: the + // service's lift handlers admit APP, and nothing else is asked of it. + ii.app_info.declared_client_class = XRT_CLIENT_CLASS_APP; + xret = ipc_client_connection_init(c, U_LOGGING_ERROR, &ii); + } + } + + os_mutex_lock(&link->mtx); + if (xret == XRT_SUCCESS) { + link->conn = c; + link->gen++; + link->state = IPC_CLIENT_LIFT_LINK_CONNECTED; + link->warned_unavailable = false; + U_LOG_W("[lift] in-process: lift-only service connection up (generation %u)", link->gen); + } else { + free(c); // init already finalised a half-made connection + link->state = IPC_CLIENT_LIFT_LINK_FAILED; + link->retry_after_ns = os_monotonic_get_ns() + LIFT_LINK_RETRY_NS; + if (!link->warned_unavailable) { + link->warned_unavailable = true; + U_LOG_W( + "[lift] in-process: no service connection for 2D->3D conversion (%s) — lift reports " + "UNAVAILABLE; retrying every %u s", + c == NULL ? "service not running" : "connect refused or failed", + (unsigned)(LIFT_LINK_RETRY_NS / 1000000000ull)); + } + } + link->connect_done = true; + os_mutex_unlock(&link->mtx); + return NULL; +} + +//! Caller holds mtx. Joins a finished connect thread. +static void +join_finished_locked(struct ipc_client_lift_link *link) +{ + if (link->thread_needs_join && link->connect_done) { + // connect_done is set in the thread's LAST critical section, so it + // never takes the mutex again: joining under it cannot deadlock, and + // holding it keeps two callers from joining the same thread. + os_thread_join(&link->thread); + os_thread_destroy(&link->thread); + link->thread_needs_join = false; + } +} + +//! Caller holds mtx. Closes a dead connection nobody holds any more. +static void +reap_dead_locked(struct ipc_client_lift_link *link) +{ + if (link->state == IPC_CLIENT_LIFT_LINK_DEAD && link->users == 0 && link->conn != NULL) { + ipc_client_connection_fini(link->conn); + free(link->conn); + link->conn = NULL; + } +} + +//! Caller holds mtx. Starts a connect when none is in flight and one is due. +static void +maybe_start_connect_locked(struct ipc_client_lift_link *link) +{ + join_finished_locked(link); + reap_dead_locked(link); + + bool due = false; + switch (link->state) { + case IPC_CLIENT_LIFT_LINK_CONNECTING: due = !link->thread_needs_join; break; // initial state + case IPC_CLIENT_LIFT_LINK_FAILED: due = os_monotonic_get_ns() >= link->retry_after_ns; break; + case IPC_CLIENT_LIFT_LINK_DEAD: due = link->conn == NULL; break; // old one closed + case IPC_CLIENT_LIFT_LINK_CONNECTED: due = false; break; + } + if (!due || link->thread_needs_join) { + return; + } + + link->state = IPC_CLIENT_LIFT_LINK_CONNECTING; + link->connect_done = false; + if (os_thread_init(&link->thread) != 0 || os_thread_start(&link->thread, connect_run, link) != 0) { + link->state = IPC_CLIENT_LIFT_LINK_FAILED; + link->retry_after_ns = os_monotonic_get_ns() + LIFT_LINK_RETRY_NS; + U_LOG_E("[lift] in-process: could not start the service connect thread"); + return; + } + link->thread_needs_join = true; +} + +xrt_result_t +ipc_client_lift_link_create(const char *label, struct ipc_client_lift_link **out_link) +{ + struct ipc_client_lift_link *link = U_TYPED_CALLOC(struct ipc_client_lift_link); + if (link == NULL) { + return XRT_ERROR_ALLOCATION; + } + if (os_mutex_init(&link->mtx) != 0) { + free(link); + return XRT_ERROR_ALLOCATION; + } + link->state = IPC_CLIENT_LIFT_LINK_CONNECTING; // nothing in flight yet: first acquire starts it + snprintf(link->label, sizeof(link->label), "%s", label != NULL ? label : "in-process lift"); + *out_link = link; + return XRT_SUCCESS; +} + +void +ipc_client_lift_link_destroy(struct ipc_client_lift_link **link_ptr) +{ + if (link_ptr == NULL || *link_ptr == NULL) { + return; + } + struct ipc_client_lift_link *link = *link_ptr; + *link_ptr = NULL; + + // An in-flight connect is bounded by the connect's own retry windows. + if (link->thread_needs_join) { + os_thread_join(&link->thread); + os_thread_destroy(&link->thread); + link->thread_needs_join = false; + } + if (link->conn != NULL) { + // Closing the pipe is what tells the service to drop every stream + // this connection owns (streams belong to the connection). + ipc_client_connection_fini(link->conn); + free(link->conn); + link->conn = NULL; + } + os_mutex_destroy(&link->mtx); + free(link); +} + +struct ipc_connection * +ipc_client_lift_link_acquire(struct ipc_client_lift_link *link, + enum ipc_client_lift_link_state *out_state, + uint32_t *out_gen) +{ + return ipc_client_lift_link_acquire_wait(link, 0, out_state, out_gen); +} + +struct ipc_connection * +ipc_client_lift_link_acquire_wait(struct ipc_client_lift_link *link, + uint32_t timeout_ms, + enum ipc_client_lift_link_state *out_state, + uint32_t *out_gen) +{ + if (out_gen != NULL) { + *out_gen = 0; + } + if (link == NULL) { + if (out_state != NULL) { + *out_state = IPC_CLIENT_LIFT_LINK_FAILED; + } + return NULL; + } + + const uint64_t deadline = os_monotonic_get_ns() + (uint64_t)timeout_ms * 1000000ull; + os_mutex_lock(&link->mtx); + maybe_start_connect_locked(link); + while (link->state == IPC_CLIENT_LIFT_LINK_CONNECTING && timeout_ms > 0 && os_monotonic_get_ns() < deadline) { + os_mutex_unlock(&link->mtx); + os_nanosleep(10 * 1000 * 1000); + os_mutex_lock(&link->mtx); + } + + struct ipc_connection *c = NULL; + if (link->state == IPC_CLIENT_LIFT_LINK_CONNECTED && link->conn != NULL) { + c = link->conn; + link->users++; + if (out_gen != NULL) { + *out_gen = link->gen; + } + } + if (out_state != NULL) { + *out_state = link->state; + } + os_mutex_unlock(&link->mtx); + return c; +} + +void +ipc_client_lift_link_release(struct ipc_client_lift_link *link, uint32_t gen, bool ipc_failed) +{ + if (link == NULL) { + return; + } + os_mutex_lock(&link->mtx); + if (link->users > 0) { + link->users--; + } + if (ipc_failed && gen == link->gen && link->state == IPC_CLIENT_LIFT_LINK_CONNECTED) { + link->state = IPC_CLIENT_LIFT_LINK_DEAD; + U_LOG_W( + "[lift] in-process: the lift service connection (generation %u) is gone — its streams are lost; " + "the session is unaffected and lift reconnects on the next call", + gen); + } + reap_dead_locked(link); + os_mutex_unlock(&link->mtx); +} diff --git a/src/xrt/ipc/client/ipc_client_lift_link.h b/src/xrt/ipc/client/ipc_client_lift_link.h new file mode 100644 index 0000000000..1ab5f8fd02 --- /dev/null +++ b/src/xrt/ipc/client/ipc_client_lift_link.h @@ -0,0 +1,103 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief XR_DXR_lift (ADR-042, ADR-049) for IN-PROCESS sessions: a lift-only + * service connection owned by the session. + * + * An in-process session renders on its own native compositor and has no IPC + * connection. Lift streams belong to an IPC connection, not to a session (the + * property `displayxr-cli lift` already relies on), so an in-process session + * reaches the service's ONE conversion module through a second, sessionless + * connection that carries lift calls only (ADR-049 option C). + * + * The link never blocks the caller on the connect: the first use starts a + * background connect (a pipe open, the version-tag handshake, at worst the + * pipe-busy / service-starting retry windows) and reports CONNECTING until it + * finishes. A failed connect is retried at most every few seconds. A connection + * that dies is DEAD: its streams are gone with it, the link reconnects on the + * next use once nobody still holds the old connection, and the GENERATION + * counter lets callers tell a stream of the dead connection from a live one. + * + * Thread-safe. The connection pointer handed out by + * ipc_client_lift_link_acquire() stays valid until the matching release. + * + * @ingroup ipc_client + */ + +#pragma once + +#include "xrt/xrt_results.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct ipc_connection; +struct ipc_client_lift_link; + +enum ipc_client_lift_link_state +{ + IPC_CLIENT_LIFT_LINK_CONNECTING = 0, //!< a connect is in flight (report ACTIVATING) + IPC_CLIENT_LIFT_LINK_CONNECTED = 1, //!< usable + IPC_CLIENT_LIFT_LINK_FAILED = 2, //!< no service / refused; retried after a back-off + IPC_CLIENT_LIFT_LINK_DEAD = 3, //!< was connected, the pipe died; reconnects on next use +}; + +/*! + * Create the link; no connect is attempted until the first + * ipc_client_lift_link_acquire(). @p label names the connection in the + * service's client list (e.g. " (in-process lift)"). + */ +xrt_result_t +ipc_client_lift_link_create(const char *label, struct ipc_client_lift_link **out_link); + +/*! + * Disconnect and free. Waits for an in-flight connect to finish (bounded by the + * connect's own retry windows). Every stream of the connection dies with it on + * the service side. NULL-safe; clears @p *link_ptr. + */ +void +ipc_client_lift_link_destroy(struct ipc_client_lift_link **link_ptr); + +/*! + * Get the connection if the link is CONNECTED, starting (or restarting, after + * the back-off) a background connect otherwise. Never blocks on the connect. + * + * @param link The link. + * @param[out] out_state The state after this call. + * @param[out] out_gen The connection generation (bumped on every new + * connection); valid when a connection is returned. + * @return The connection, which the caller must hand back with + * ipc_client_lift_link_release(); NULL unless CONNECTED. + */ +struct ipc_connection * +ipc_client_lift_link_acquire(struct ipc_client_lift_link *link, + enum ipc_client_lift_link_state *out_state, + uint32_t *out_gen); + +/*! + * Like ipc_client_lift_link_acquire(), but when a connect is in flight wait up + * to @p timeout_ms for it to finish. For setup calls (stream creation), never + * for per-frame ones. + */ +struct ipc_connection * +ipc_client_lift_link_acquire_wait(struct ipc_client_lift_link *link, + uint32_t timeout_ms, + enum ipc_client_lift_link_state *out_state, + uint32_t *out_gen); + +/*! + * Hand back a connection from acquire. @p ipc_failed: the call made on it + * returned XRT_ERROR_IPC_FAILURE — the link turns DEAD (one WARN) and the + * connection is closed once its last user has released it. + */ +void +ipc_client_lift_link_release(struct ipc_client_lift_link *link, uint32_t gen, bool ipc_failed); + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/state_trackers/oxr/oxr_lift.c b/src/xrt/state_trackers/oxr/oxr_lift.c index 36df37147d..68bba0fad2 100644 --- a/src/xrt/state_trackers/oxr/oxr_lift.c +++ b/src/xrt/state_trackers/oxr/oxr_lift.c @@ -7,15 +7,28 @@ * @ingroup oxr_api * * The conversion runs in the service (d3d11_lift.cpp, its own thread and - * device); these entry points validate, forward to thin IPC-client bridges - * (ipc_client_compositor.c — st_oxr does not pull the ipc_client include path, - * so the symbols resolve at link time, the oxr_weave.c pattern) and translate - * results. IPC-only, like XR_DXR_weave: an in-process session reports - * XR_ERROR_FEATURE_UNSUPPORTED from every entry point. + * device); these entry points validate, call ipc_client_lift.c on a service + * connection and translate results. Lift streams belong to a CONNECTION, not a + * session, so there are two routes to the one conversion module: + * + * - an IPC session uses its own connection (ipc_client_compositor.c hands it + * over — the symbol resolves at link time, the oxr_weave.c pattern); + * - an in-process D3D11 / D3D12 session on Windows (engine apps — the Unity + * display provider renders in-process on its own D3D12 device) uses a + * lift-only connection the session owns (ipc_client_lift_link.c), opened in + * the background on first use (ADR-049). Results come back as the same NT + * handles (texture + ID3D11Fence-created shared fence) a D3D12 device opens + * with OpenSharedHandle. + * + * Any other in-process session reports XR_ERROR_FEATURE_UNSUPPORTED. * * Error contract (mirrors XR_DXR_weave §4b): - * - a dead pipe (XRT_ERROR_IPC_FAILURE) marks the session lost → + * - IPC session: a dead pipe (XRT_ERROR_IPC_FAILURE) marks the session lost → * XR_ERROR_INSTANCE_LOST, then XR_ERROR_SESSION_LOST; + * - in-process session: a dead lift connection never touches the session — + * the call is a transient XR_ERROR_RUNTIME_FAILURE, streams of that + * connection then report XR_ERROR_FEATURE_UNSUPPORTED (recreate them), and + * properties reconnect in the background; * - a transient service refusal (XRT_ERROR_WEAVE_REFUSED — keyed-mutex miss) * is a non-fatal XR_ERROR_RUNTIME_FAILURE, retry next frame; * - no module / mode unsupported (XRT_ERROR_FEATURE_NOT_SUPPORTED) is @@ -34,76 +47,28 @@ #include "util/u_trace_marker.h" #include "util/u_logging.h" +#include "xrt/xrt_instance.h" #include "xrt/xrt_lift.h" +// st_oxr carries the ipc include path (oxr_workspace.c); these two headers +// need nothing generated. +#include "client/ipc_client_lift.h" +#include "client/ipc_client_lift_link.h" + +#ifdef XRT_OS_WINDOWS +#include "xrt/xrt_windows.h" +#endif + #include +#include #include #include #ifdef OXR_HAVE_DXR_lift -// IPC-bridge wrappers (defined in ipc_client_compositor.c). -xrt_result_t -comp_ipc_client_compositor_lift_get_properties(struct xrt_compositor *xc, struct xrt_dp_lift_caps *out_caps); -xrt_result_t -comp_ipc_client_compositor_lift_stream_create(struct xrt_compositor *xc, - uint32_t mode, - uint32_t content_hint, - float input_scale, - uint32_t aux_outputs, - uint32_t aux_depth_format, - uint64_t *out_stream_id); -xrt_result_t -comp_ipc_client_compositor_lift_stream_destroy(struct xrt_compositor *xc, uint64_t stream_id); -xrt_result_t -comp_ipc_client_compositor_lift_submit(struct xrt_compositor *xc, - uint64_t stream_id, - xrt_graphics_buffer_handle_t handle, - bool is_dxgi, - uint32_t width, - uint32_t height, - int64_t source_time, - const struct xrt_dp_lift_params *params, - const float *viewpoints, - uint32_t viewpoint_count, - const struct xrt_lift_view_control *view_control, - const struct xrt_lift_rig *rig, - uint64_t *out_frame_id); -xrt_result_t -comp_ipc_client_compositor_lift_acquire(struct xrt_compositor *xc, - uint64_t stream_id, - bool *out_ready, - struct xrt_lift_result *out_result); -xrt_result_t -comp_ipc_client_compositor_lift_get_output(struct xrt_compositor *xc, - uint64_t stream_id, - bool *out_have, - xrt_graphics_buffer_handle_t *out_handle); -xrt_result_t -comp_ipc_client_compositor_lift_get_depth_output(struct xrt_compositor *xc, - uint64_t stream_id, - bool *out_have, - xrt_graphics_buffer_handle_t *out_handle); -xrt_result_t -comp_ipc_client_compositor_lift_get_fence(struct xrt_compositor *xc, - uint64_t stream_id, - bool *out_have, - xrt_graphics_sync_handle_t *out_handle); -xrt_result_t -comp_ipc_client_compositor_lift_acquire_blob(struct xrt_compositor *xc, - uint64_t stream_id, - uint64_t capacity, - uint8_t *out_bytes, - bool *out_ready, - bool *out_delivered, - struct xrt_lift_blob_info *out_info); - -xrt_result_t -comp_ipc_client_compositor_lift_set_priority(struct xrt_compositor *xc, uint64_t stream_id, uint32_t priority); -xrt_result_t -comp_ipc_client_compositor_lift_stats(struct xrt_compositor *xc, - uint64_t stream_id, - struct xrt_lift_stream_stats *out_stats); +// IPC-session bridge (defined in ipc_client_compositor.c): the session's connection. +struct ipc_connection * +comp_ipc_client_compositor_lift_connection(struct xrt_compositor *xc); // The OpenXR and DP encodings must agree (the wire carries the DP one). _Static_assert(XR_LIFT_MODE_DEPTH_DXR == XRT_DP_LIFT_MODE_DEPTH, "lift mode mismatch"); @@ -138,13 +103,164 @@ lift_session_is_ipc(struct oxr_session *sess) return !inprocess; } +/*! + * ADR-049: which in-process sessions reach the service's module over a + * lift-only connection. The conversion service is the Windows D3D11 service + * and its results are D3D NT handles, which a D3D11 or D3D12 device opens + * directly; other in-process APIs keep XR_ERROR_FEATURE_UNSUPPORTED. + */ +static bool +lift_inprocess_eligible(struct oxr_session *sess) +{ +#ifdef XRT_OS_WINDOWS + return sess->is_d3d11_native_compositor || sess->is_d3d12_native_compositor; +#else + (void)sess; + return false; +#endif +} + +//! The session's lift link, created on first use (no connect yet). +static struct ipc_client_lift_link * +lift_link_get(struct oxr_session *sess) +{ +#ifdef XRT_OS_WINDOWS + struct ipc_client_lift_link *link = (struct ipc_client_lift_link *)InterlockedCompareExchangePointer( + (PVOID volatile *)&sess->lift_link, NULL, NULL); + if (link != NULL) { + return link; + } + + // Name the connection after the executable so `displayxr-cli clients` and + // the service log say whose it is. + char exe[MAX_PATH] = {0}; + const char *base = "app"; + if (GetModuleFileNameA(NULL, exe, (DWORD)sizeof(exe)) > 0) { + const char *slash = strrchr(exe, '\\'); + base = slash != NULL ? slash + 1 : exe; + } + char label[XRT_MAX_APPLICATION_NAME_SIZE]; + snprintf(label, sizeof(label), "%s (in-process lift)", base); + + struct ipc_client_lift_link *made = NULL; + if (ipc_client_lift_link_create(label, &made) != XRT_SUCCESS) { + return NULL; + } + // Two threads may race the first lift call of a session: one link wins. + link = (struct ipc_client_lift_link *)InterlockedCompareExchangePointer((PVOID volatile *)&sess->lift_link, + made, NULL); + if (link != NULL) { + ipc_client_lift_link_destroy(&made); + return link; + } + return made; +#else + (void)sess; + return NULL; +#endif +} + +void +oxr_lift_session_fini(struct oxr_session *sess) +{ + if (sess != NULL && sess->lift_link != NULL) { + ipc_client_lift_link_destroy(&sess->lift_link); + } +} + +//! The connection one lift call rides, and how its failure is treated. +struct lift_route +{ + struct ipc_connection *c; //!< NULL = no connection right now (in-process only) + bool inprocess; //!< the session's lift-only link (ADR-049) + uint32_t gen; //!< link generation the connection belongs to + enum ipc_client_lift_link_state state; +}; + +/*! + * Pick the route. IPC session: its own connection, always. Eligible in-process + * session: the lift link; @p wait_ms bounds a wait for an in-flight connect + * (0 for everything but stream creation). On XR_SUCCESS the caller must + * lift_route_close() it; route->c may still be NULL in-process (see state). + */ +static XrResult +lift_route_open( + struct oxr_logger *log, struct oxr_session *sess, uint32_t wait_ms, struct lift_route *route, const char *what) +{ + memset(route, 0, sizeof(*route)); + if (lift_session_is_ipc(sess)) { + route->c = comp_ipc_client_compositor_lift_connection(&sess->xcn->base); + route->state = IPC_CLIENT_LIFT_LINK_CONNECTED; + return XR_SUCCESS; + } + if (!lift_inprocess_eligible(sess)) { + return oxr_error(log, XR_ERROR_FEATURE_UNSUPPORTED, + "%s: in-process 2D→3D conversion is available to D3D11 / D3D12 sessions on Windows " + "only (other sessions: the out-of-process service path)", + what); + } + struct ipc_client_lift_link *link = lift_link_get(sess); + if (link == NULL) { + return oxr_error(log, XR_ERROR_RUNTIME_FAILURE, "%s: could not create the lift service link", what); + } + route->inprocess = true; + route->c = ipc_client_lift_link_acquire_wait(link, wait_ms, &route->state, &route->gen); + return XR_SUCCESS; +} + +static void +lift_route_close(struct oxr_session *sess, struct lift_route *route, xrt_result_t last_xret) +{ + if (route->inprocess && route->c != NULL) { + ipc_client_lift_link_release(sess->lift_link, route->gen, last_xret == XRT_ERROR_IPC_FAILURE); + } + route->c = NULL; +} + +/*! + * A stream call's route: the stream's own connection, or -- in-process, when + * that connection died -- XR_ERROR_FEATURE_UNSUPPORTED (the stream is gone with + * it; the app destroys it and creates a new one). + */ +static XrResult +lift_stream_route_open(struct oxr_logger *log, + struct oxr_lift_stream_dxr *st, + struct lift_route *route, + const char *what) +{ + XrResult r = lift_route_open(log, st->sess, 0, route, what); + if (r != XR_SUCCESS) { + return r; + } + if (route->inprocess && (route->c == NULL || route->gen != st->link_gen)) { + lift_route_close(st->sess, route, XRT_SUCCESS); + return oxr_error(log, XR_ERROR_FEATURE_UNSUPPORTED, + "%s: this stream belonged to a lift service connection that is gone - destroy it and " + "create a new one once xrGetLiftPropertiesDXR reports READY", + what); + } + return XR_SUCCESS; +} + //! Map a service result; returns XR_SUCCESS only for XRT_SUCCESS. static XrResult -lift_xret(struct oxr_logger *log, struct oxr_session *sess, xrt_result_t xret, const char *what) +lift_xret(struct oxr_logger *log, + struct oxr_session *sess, + const struct lift_route *route, + xrt_result_t xret, + const char *what) { switch (xret) { case XRT_SUCCESS: return XR_SUCCESS; case XRT_ERROR_IPC_FAILURE: + if (route->inprocess) { + // The session renders in-process: losing the lift-only connection + // loses its streams, never the session (ADR-049). + return oxr_error(log, XR_ERROR_RUNTIME_FAILURE, + "%s: the lift service connection is gone (session unaffected; recreate lift " + "streams once xrGetLiftPropertiesDXR reports READY)", + what); + } sess->has_lost = true; return oxr_error(log, XR_ERROR_INSTANCE_LOST, "%s: the runtime service connection is gone", what); case XRT_ERROR_FEATURE_NOT_SUPPORTED: @@ -273,7 +389,14 @@ oxr_lift_stream_destroy_cb(struct oxr_logger *log, struct oxr_handle_base *hb) struct oxr_lift_stream_dxr *st = (struct oxr_lift_stream_dxr *)hb; // Best effort: a dead connection already took every stream with it. if (st->sess != NULL && st->sess->xcn != NULL && !st->sess->has_lost) { - (void)comp_ipc_client_compositor_lift_stream_destroy(&st->sess->xcn->base, st->id); + struct lift_route route; + if (lift_route_open(log, st->sess, 0, &route, "xrDestroyLiftStreamDXR") == XR_SUCCESS) { + xrt_result_t xret = XRT_SUCCESS; + if (route.c != NULL && (!route.inprocess || route.gen == st->link_gen)) { + xret = ipc_client_lift_stream_destroy(route.c, st->id); + } + lift_route_close(st->sess, &route, xret); + } } free(st); return XR_SUCCESS; @@ -298,17 +421,26 @@ oxr_xrGetLiftPropertiesDXR(XrSession session, XrLiftPropertiesDXR *properties) OXR_VERIFY_EXTENSION(&log, sess->sys->inst, DXR_lift); OXR_VERIFY_ARG_TYPE_AND_NOT_NULL(&log, properties, XR_TYPE_LIFT_PROPERTIES_DXR); - if (!lift_session_is_ipc(sess)) { - return oxr_error(&log, XR_ERROR_FEATURE_UNSUPPORTED, - "xrGetLiftPropertiesDXR: 2D→3D conversion is only available on the out-of-process " - "(service) path"); + struct lift_route route; + XrResult r = lift_route_open(&log, sess, 0, &route, "xrGetLiftPropertiesDXR"); + if (r != XR_SUCCESS) { + return r; } struct xrt_dp_lift_caps caps; - xrt_result_t xret = comp_ipc_client_compositor_lift_get_properties(&sess->xcn->base, &caps); - XrResult r = lift_xret(&log, sess, xret, "xrGetLiftPropertiesDXR"); - if (r != XR_SUCCESS) { - return r; + xrt_dp_lift_caps_init(&caps); + if (route.c == NULL) { + // In-process, no connection right now (ADR-049): a connect in flight + // reads ACTIVATING (poll), no service reads UNAVAILABLE (open default). + caps.state = route.state == IPC_CLIENT_LIFT_LINK_CONNECTING ? XRT_DP_LIFT_STATE_ACTIVATING + : XRT_DP_LIFT_STATE_UNAVAILABLE; + } else { + xrt_result_t xret = ipc_client_lift_get_properties(route.c, &caps); + lift_route_close(sess, &route, xret); + r = lift_xret(&log, sess, &route, xret, "xrGetLiftPropertiesDXR"); + if (r != XR_SUCCESS) { + return r; + } } properties->supportedModes = (XrLiftModeFlagsDXR)caps.modes; properties->maxStreams = caps.max_streams; @@ -382,16 +514,31 @@ oxr_xrCreateLiftStreamDXR(XrSession session, const XrLiftStreamCreateInfoDXR *cr ? (uint32_t)dreq->preferredFormat : 0u; } - if (!lift_session_is_ipc(sess)) { + // Creation is a setup call: in-process it may wait for the background + // connect (bounded by the connect's own service-starting window). + struct lift_route route; + XrResult r = lift_route_open(&log, sess, 6000, &route, "xrCreateLiftStreamDXR"); + if (r != XR_SUCCESS) { + return r; + } + if (route.c == NULL) { + if (route.state == IPC_CLIENT_LIFT_LINK_CONNECTING) { + return oxr_error( + &log, XR_ERROR_RUNTIME_FAILURE, + "xrCreateLiftStreamDXR: the lift service connection is still being made - retry"); + } return oxr_error(&log, XR_ERROR_FEATURE_UNSUPPORTED, - "xrCreateLiftStreamDXR: only available on the out-of-process (service) path"); + "xrCreateLiftStreamDXR: no service to convert with (xrGetLiftPropertiesDXR reports " + "UNAVAILABLE)"); } uint64_t id = 0; - xrt_result_t xret = comp_ipc_client_compositor_lift_stream_create( - &sess->xcn->base, mode, (uint32_t)createInfo->contentHint, - createInfo->inputScale > 0.0f ? createInfo->inputScale : 1.0f, aux_outputs, aux_depth_format, &id); - XrResult r = lift_xret(&log, sess, xret, "xrCreateLiftStreamDXR"); + xrt_result_t xret = ipc_client_lift_stream_create(route.c, mode, (uint32_t)createInfo->contentHint, + createInfo->inputScale > 0.0f ? createInfo->inputScale : 1.0f, + aux_outputs, aux_depth_format, &id); + const uint32_t gen = route.gen; + lift_route_close(sess, &route, xret); + r = lift_xret(&log, sess, &route, xret, "xrCreateLiftStreamDXR"); if (r != XR_SUCCESS) { return r; } @@ -402,6 +549,7 @@ oxr_xrCreateLiftStreamDXR(XrSession session, const XrLiftStreamCreateInfoDXR *cr st->id = id; st->mode = mode; st->depth_requested = aux_outputs != 0; + st->link_gen = gen; *stream = XRT_CAST_PTR_TO_OXR_HANDLE(XrLiftStreamDXR, st); return XR_SUCCESS; } @@ -479,21 +627,30 @@ oxr_xrSubmitLiftFrameDXR(XrLiftStreamDXR stream, const XrLiftFrameSubmitInfoDXR } } - xrt_result_t xret = comp_ipc_client_compositor_lift_submit( - &sess->xcn->base, st->id, (xrt_graphics_buffer_handle_t)(intptr_t)submitInfo->inputTexture, + struct lift_route route; + XrResult rr = lift_stream_route_open(&log, st, &route, "xrSubmitLiftFrameDXR"); + if (rr != XR_SUCCESS) { + return rr; + } + xrt_result_t xret = ipc_client_lift_submit( + route.c, st->id, (xrt_graphics_buffer_handle_t)(intptr_t)submitInfo->inputTexture, submitInfo->inputIsDxgi == XR_TRUE, (uint32_t)submitInfo->extent.width, (uint32_t)submitInfo->extent.height, (int64_t)submitInfo->sourceTime, pp, vp_count > 0 ? vps : NULL, vp_count, vcp, rigp, frameId); - return lift_xret(&log, sess, xret, "xrSubmitLiftFrameDXR"); + lift_route_close(sess, &route, xret); + return lift_xret(&log, sess, &route, xret, "xrSubmitLiftFrameDXR"); } //! Fill an XrLiftDepthResultDXR from an acquired result (+ handles on first -//! depth acquire / reallocation). +//! depth acquire / reallocation) over the result's @p route; on a failed +//! export *out_xret carries the service result for lift_route_close(). static XrResult lift_fill_depth_result(struct oxr_logger *log, struct oxr_session *sess, struct oxr_lift_stream_dxr *st, + const struct lift_route *route, const struct xrt_lift_result *r, - XrLiftDepthResultDXR *d) + XrLiftDepthResultDXR *d, + xrt_result_t *out_xret) { XrStructureType type = d->type; void *next = d->next; @@ -537,15 +694,15 @@ lift_fill_depth_result(struct oxr_logger *log, xrt_graphics_buffer_handle_t th = XRT_GRAPHICS_BUFFER_HANDLE_INVALID; xrt_graphics_sync_handle_t fh = XRT_GRAPHICS_SYNC_HANDLE_INVALID; xrt_result_t xret = - comp_ipc_client_compositor_lift_get_depth_output(&sess->xcn->base, st->id, &have_tex, &th); - XrResult xr = lift_xret(log, sess, xret, "xrAcquireLiftResultDXR (depth export)"); - if (xr != XR_SUCCESS) { - return xr; + ipc_client_lift_get_depth_output(route->c, st->id, &have_tex, NULL, NULL, NULL, &th); + if (xret != XRT_SUCCESS) { + *out_xret = xret; + return lift_xret(log, sess, route, xret, "xrAcquireLiftResultDXR (depth export)"); } - xret = comp_ipc_client_compositor_lift_get_fence(&sess->xcn->base, st->id, &have_fence, &fh); - xr = lift_xret(log, sess, xret, "xrAcquireLiftResultDXR (depth fence export)"); - if (xr != XR_SUCCESS) { - return xr; + xret = ipc_client_lift_get_fence(route->c, st->id, &have_fence, &fh); + if (xret != XRT_SUCCESS) { + *out_xret = xret; + return lift_xret(log, sess, route, xret, "xrAcquireLiftResultDXR (depth fence export)"); } const bool got_tex = have_tex && th != XRT_GRAPHICS_BUFFER_HANDLE_INVALID; const bool got_fence = have_fence && fh != XRT_GRAPHICS_SYNC_HANDLE_INVALID; @@ -580,15 +737,19 @@ oxr_xrAcquireLiftResultDXR(XrLiftStreamDXR stream, XrLiftResultDXR *result) result->outputTexture = NULL; result->fence = NULL; - bool ready = false; - struct xrt_lift_result r; - xrt_result_t xret = comp_ipc_client_compositor_lift_acquire(&sess->xcn->base, st->id, &ready, &r); - XrResult xr = lift_xret(&log, sess, xret, "xrAcquireLiftResultDXR"); + struct lift_route route; + XrResult xr = lift_stream_route_open(&log, st, &route, "xrAcquireLiftResultDXR"); if (xr != XR_SUCCESS) { return xr; } - if (!ready) { - return XR_LIFT_NOT_READY_DXR; + + bool ready = false; + struct xrt_lift_result r; + xrt_result_t xret = ipc_client_lift_acquire(route.c, st->id, &ready, &r); + if (xret != XRT_SUCCESS || !ready) { + lift_route_close(sess, &route, xret); + xr = lift_xret(&log, sess, &route, xret, "xrAcquireLiftResultDXR"); + return xr != XR_SUCCESS ? xr : XR_LIFT_NOT_READY_DXR; } result->frameId = r.frame_id; @@ -623,15 +784,15 @@ oxr_xrAcquireLiftResultDXR(XrLiftStreamDXR stream, XrLiftResultDXR *result) bool have_tex = false, have_fence = false; xrt_graphics_buffer_handle_t th = XRT_GRAPHICS_BUFFER_HANDLE_INVALID; xrt_graphics_sync_handle_t fh = XRT_GRAPHICS_SYNC_HANDLE_INVALID; - xret = comp_ipc_client_compositor_lift_get_output(&sess->xcn->base, st->id, &have_tex, &th); - xr = lift_xret(&log, sess, xret, "xrAcquireLiftResultDXR (output export)"); - if (xr != XR_SUCCESS) { - return xr; + xret = ipc_client_lift_get_output(route.c, st->id, &have_tex, NULL, NULL, NULL, &th); + if (xret != XRT_SUCCESS) { + lift_route_close(sess, &route, xret); + return lift_xret(&log, sess, &route, xret, "xrAcquireLiftResultDXR (output export)"); } - xret = comp_ipc_client_compositor_lift_get_fence(&sess->xcn->base, st->id, &have_fence, &fh); - xr = lift_xret(&log, sess, xret, "xrAcquireLiftResultDXR (fence export)"); - if (xr != XR_SUCCESS) { - return xr; + xret = ipc_client_lift_get_fence(route.c, st->id, &have_fence, &fh); + if (xret != XRT_SUCCESS) { + lift_route_close(sess, &route, xret); + return lift_xret(&log, sess, &route, xret, "xrAcquireLiftResultDXR (fence export)"); } const bool got_tex = have_tex && th != XRT_GRAPHICS_BUFFER_HANDLE_INVALID; const bool got_fence = have_fence && fh != XRT_GRAPHICS_SYNC_HANDLE_INVALID; @@ -649,11 +810,14 @@ oxr_xrAcquireLiftResultDXR(XrLiftStreamDXR stream, XrLiftResultDXR *result) XrLiftDepthResultDXR *dres = OXR_GET_OUTPUT_FROM_CHAIN(result, XR_TYPE_LIFT_DEPTH_RESULT_DXR, XrLiftDepthResultDXR); if (dres != NULL) { - xr = lift_fill_depth_result(&log, sess, st, &r, dres); + xret = XRT_SUCCESS; + xr = lift_fill_depth_result(&log, sess, st, &route, &r, dres, &xret); if (xr != XR_SUCCESS) { + lift_route_close(sess, &route, xret); return xr; } } + lift_route_close(sess, &route, XRT_SUCCESS); return XR_SUCCESS; } @@ -677,11 +841,17 @@ oxr_xrAcquireLiftBlobDXR(XrLiftStreamDXR stream, XrLiftBlobDXR *blob) return oxr_error(&log, XR_ERROR_VALIDATION_FAILURE, "XrLiftBlobDXR::bytes is NULL with a capacity"); } + struct lift_route route; + XrResult xr = lift_stream_route_open(&log, st, &route, "xrAcquireLiftBlobDXR"); + if (xr != XR_SUCCESS) { + return xr; + } bool ready = false, delivered = false; struct xrt_lift_blob_info info; - xrt_result_t xret = comp_ipc_client_compositor_lift_acquire_blob( - &sess->xcn->base, st->id, blob->byteCapacityInput, blob->bytes, &ready, &delivered, &info); - XrResult xr = lift_xret(&log, sess, xret, "xrAcquireLiftBlobDXR"); + xrt_result_t xret = ipc_client_lift_acquire_blob(route.c, st->id, blob->byteCapacityInput, blob->bytes, &ready, + &delivered, &info); + lift_route_close(sess, &route, xret); + xr = lift_xret(&log, sess, &route, xret, "xrAcquireLiftBlobDXR"); if (xr != XR_SUCCESS) { return xr; } @@ -720,8 +890,14 @@ oxr_xrSetLiftStreamPriorityDXR(XrLiftStreamDXR stream, XrLiftPriorityDXR priorit return oxr_error(&log, XR_ERROR_VALIDATION_FAILURE, "xrSetLiftStreamPriorityDXR: priority (%d) invalid", (int)priority); } - xrt_result_t xret = comp_ipc_client_compositor_lift_set_priority(&sess->xcn->base, st->id, (uint32_t)priority); - return lift_xret(&log, sess, xret, "xrSetLiftStreamPriorityDXR"); + struct lift_route route; + XrResult xr = lift_stream_route_open(&log, st, &route, "xrSetLiftStreamPriorityDXR"); + if (xr != XR_SUCCESS) { + return xr; + } + xrt_result_t xret = ipc_client_lift_set_priority(route.c, st->id, (uint32_t)priority); + lift_route_close(sess, &route, xret); + return lift_xret(&log, sess, &route, xret, "xrSetLiftStreamPriorityDXR"); } XRAPI_ATTR XrResult XRAPI_CALL @@ -736,9 +912,15 @@ oxr_xrGetLiftStreamStatsDXR(XrLiftStreamDXR stream, XrLiftStreamStatsDXR *stats) OXR_VERIFY_SESSION_NOT_LOST(&log, sess); OXR_VERIFY_ARG_TYPE_AND_NOT_NULL(&log, stats, XR_TYPE_LIFT_STREAM_STATS_DXR); + struct lift_route route; + XrResult xr = lift_stream_route_open(&log, st, &route, "xrGetLiftStreamStatsDXR"); + if (xr != XR_SUCCESS) { + return xr; + } struct xrt_lift_stream_stats s; - xrt_result_t xret = comp_ipc_client_compositor_lift_stats(&sess->xcn->base, st->id, &s); - XrResult xr = lift_xret(&log, sess, xret, "xrGetLiftStreamStatsDXR"); + xrt_result_t xret = ipc_client_lift_stats(route.c, st->id, &s); + lift_route_close(sess, &route, xret); + xr = lift_xret(&log, sess, &route, xret, "xrGetLiftStreamStatsDXR"); if (xr != XR_SUCCESS) { return xr; } diff --git a/src/xrt/state_trackers/oxr/oxr_objects.h b/src/xrt/state_trackers/oxr/oxr_objects.h index 3bdeea3522..9f5e063e10 100644 --- a/src/xrt/state_trackers/oxr/oxr_objects.h +++ b/src/xrt/state_trackers/oxr/oxr_objects.h @@ -137,6 +137,7 @@ struct oxr_xdev_list; struct oxr_plane_detector_ext; struct oxr_local_3d_zone_ext; struct oxr_lift_stream_dxr; +struct ipc_client_lift_link; struct oxr_stereo_camera_stream_dxr; #define XRT_MAX_HANDLE_CHILDREN 256 @@ -2840,6 +2841,14 @@ struct oxr_session //! True if using GL native compositor (not multi_compositor). bool is_gl_native_compositor; + /*! + * XR_DXR_lift for an IN-PROCESS session (ADR-049): the lift-only service + * connection, created on the first lift call and freed after the + * session's lift streams (its children). NULL on IPC sessions, whose + * lift calls ride the session's own connection. + */ + struct ipc_client_lift_link *lift_link; + //! True if session was created with an external window handle (XR_DXR_win32_window_binding). bool has_external_window; @@ -4483,7 +4492,18 @@ struct oxr_lift_stream_dxr bool exported; //!< the caller holds the current export texture + fence bool depth_requested; //!< XR_DXR_lift v3: created with XrLiftDepthRequestDXR bool depth_exported; //!< the caller holds the current depth texture + fence + //! In-process session (ADR-049): the lift link generation the stream was + //! created on. A stream of a dead generation died with that connection. + uint32_t link_gen; }; + +/*! + * Free an in-process session's lift-only service connection (ADR-049). + * Called from the session's destroy, after its lift streams (children) are + * gone. No-op for an IPC session or one that never used lift. + */ +void +oxr_lift_session_fini(struct oxr_session *sess); #endif // OXR_HAVE_DXR_lift #ifdef OXR_HAVE_DXR_stereo_camera diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index 6c8ea6afd2..0bd357091b 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -4992,6 +4992,12 @@ oxr_session_destroy(struct oxr_logger *log, struct oxr_handle_base *hb) // MCP tool handler stops reading. oxr_mcp_tools_detach_session(sess); +#ifdef OXR_HAVE_DXR_lift + // ADR-049: the lift streams (children) are already destroyed; close the + // in-process session's lift-only service connection behind them. + oxr_lift_session_fini(sess); +#endif + #ifdef OXR_HAVE_DXR_mcp_tools // Unregister the session's app-defined tools (XR_DXR_mcp_tools) and // fail their pending calls before the event queue is drained below.