--- /mnt/data/quest_3_mr_phase_shift_encoder_metric.html	2026-06-21 19:02:21.927373075 +0000
+++ /mnt/data/quest_3_mr_phase_shift_encoder_adaptive_v4.html	2026-06-21 19:14:53.771932781 +0000
@@ -4,7 +4,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
     <meta name="color-scheme" content="dark">
-    <title>Metric MR Phase-Shift RGB-D Encoder</title>
+    <title>Adaptive Mono/Stereo Metric MR RGB-D Encoder</title>
     <style>
         :root {
             color-scheme: dark;
@@ -145,7 +145,7 @@
                 RGB layout
                 <select id="rgbLayout">
                     <option value="auto" selected>Auto</option>
-                    <option value="mono">Mono (duplicate)</option>
+                    <option value="mono">Mono source</option>
                     <option value="sbs">Side-by-side stereo</option>
                 </select>
             </label>
@@ -170,30 +170,31 @@
         </div>
 
         <div id="status">Ready. Serve this file from HTTPS (or localhost), then start it from a user gesture.</div>
-        <div class="hint">In XR, a controller trigger also requests a snapshot. The PNG embeds metadata plus exact native left/right depth buffers. Phase max depth affects only the atlas fallback; no simulated Z is ever substituted.</div>
+        <div class="hint">Phone/touch, hand, gaze, and controller primary actions can request a snapshot. Mono XR writes one calibrated view; stereo XR writes two. The PNG embeds per-view metric depth, view/sensor transforms, and explicit stereo baselines when present.</div>
     </section>
 
     <script>
 (() => {
     'use strict';
 
-    // Self-contained calibrated RGB-D snapshot format:
+    // Self-contained calibrated mono/stereo RGB-D snapshot format:
     //   2048 x 2048 PNG
-    //   top-left / top-right: 1024 x 1024 RGB
-    //   bottom-left / bottom-right: 1024 x 1024 phase-shift depth atlases
+    //   mono: left RGB/depth slots used; right slots intentionally empty
+    //   stereo: left/right RGB and depth slots used
     //   PNG iTXt: full reconstruction metadata
-    //   PNG private ancillary chunks mdPL / mdPR: exact native WebXR CPU depth buffers
+    //   PNG private ancillary chunks mdPN (mono) or mdPL/mdPR (stereo): exact native WebXR CPU depth buffers
     const OUTPUT_SIZE = 2048;
     const EYE_SIZE = 1024;
     const TILE_SIZE = 512;
     const DEPTH_PREVIEW_INTERVAL_MS = 850;
-    const AUTO_SBS_ASPECT_THRESHOLD = 2.2;
+    const AUTO_SBS_ASPECT_THRESHOLD = 1.9;
     const METADATA_KEYWORD = 'mr-phase-shift-metadata';
     const RAW_DEPTH_HEADER_BYTES = 28;
     const DEPTH_ALPHA_INVALID = 0;
     const DEPTH_ALPHA_CLIPPED = 128;
     const DEPTH_ALPHA_VALID = 255;
     const VIDEO_FRAME_TIMEOUT_MS = 1200;
+    const MAX_CAPTURE_VIEWS = 2;
 
     const previewCanvas = document.getElementById('preview');
     const previewCtx = previewCanvas.getContext('2d', { alpha: false });
@@ -228,12 +229,13 @@
         pendingRgbCapture: null,
         previewRaf: 0,
         lastDepthCopyAt: -Infinity,
-        latestDepth: { left: null, right: null },
+        latestDepth: null,
         encodedDepth: { left: null, right: null },
         cameraSettings: null,
         depthSessionInfo: null,
         frameCounter: 0,
         pendingEncodeGeneration: 0,
+        pendingTrigger: null,
     };
 
     function setStatus(message, kind = '') {
@@ -249,6 +251,33 @@
         return Number.isFinite(value) && value > 0 ? value : fallback;
     }
 
+    function captureSetReady(capture) {
+        return !!(capture && (
+            (capture.mode === 'mono' && capture.viewCount === 1 && capture.left) ||
+            (capture.mode === 'stereo' && capture.viewCount === 2 && capture.left && capture.right)
+        ));
+    }
+
+    function describeCaptureTrigger(event) {
+        if (event && event.inputSource) {
+            return {
+                type: 'webxr-select',
+                eventType: event.type || 'select',
+                targetRayMode: event.inputSource.targetRayMode || null,
+                handedness: event.inputSource.handedness || 'none',
+                profiles: event.inputSource.profiles ? Array.from(event.inputSource.profiles) : [],
+            };
+        }
+        if (event && event.type) {
+            return {
+                type: 'dom-event',
+                eventType: event.type,
+                pointerType: event.pointerType || null,
+            };
+        }
+        return { type: 'programmatic', eventType: null };
+    }
+
     function getEncodingOptions() {
         return {
             fitMode: fitModeSelect.value === 'cover' ? 'cover' : 'contain',
@@ -341,15 +370,44 @@
         return fallback ? new Float32Array(fallback) : null;
     }
 
-    function getRgbLayout(sourceWidth, sourceHeight) {
+    function matrixOrigin(matrix) {
+        return [matrix[12], matrix[13], matrix[14]];
+    }
+
+    function subtractVectors(to, from) {
+        return [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
+    }
+
+    function vectorLength(vector) {
+        return Math.hypot(vector[0], vector[1], vector[2]);
+    }
+
+    function computeCaptureBaselines(capture) {
+        if (!capture || capture.mode !== 'stereo' || !capture.left || !capture.right) return null;
+        const viewVector = subtractVectors(
+            matrixOrigin(capture.right.captureLocalFromView),
+            matrixOrigin(capture.left.captureLocalFromView));
+        const sensorVector = subtractVectors(
+            matrixOrigin(capture.right.captureLocalFromSensor),
+            matrixOrigin(capture.left.captureLocalFromSensor));
+        return {
+            viewLeftToRightMeters: viewVector,
+            viewOriginDistanceMeters: vectorLength(viewVector),
+            depthSensorLeftToRightMeters: sensorVector,
+            depthSensorOriginDistanceMeters: vectorLength(sensorVector),
+        };
+    }
+
+    function getRgbLayout(sourceWidth, sourceHeight, captureMode = 'stereo') {
+        if (captureMode === 'mono') return 'mono';
         const selected = rgbLayoutSelect.value;
         if (selected === 'mono' || selected === 'sbs') return selected;
         const aspect = sourceWidth / Math.max(1, sourceHeight);
         return aspect >= AUTO_SBS_ASPECT_THRESHOLD ? 'sbs' : 'mono';
     }
 
-    function getRgbEyeSourceRect(sourceWidth, sourceHeight, eye) {
-        const layout = getRgbLayout(sourceWidth, sourceHeight);
+    function getRgbEyeSourceRect(sourceWidth, sourceHeight, eye, captureMode = 'stereo') {
+        const layout = getRgbLayout(sourceWidth, sourceHeight, captureMode);
         if (layout === 'sbs') {
             const leftWidth = Math.floor(sourceWidth / 2);
             const rightWidth = sourceWidth - leftWidth;
@@ -409,12 +467,15 @@
         };
     }
 
-    function buildRgbMappings(sourceWidth, sourceHeight, fitMode) {
+    function buildRgbMappings(sourceWidth, sourceHeight, fitMode, captureMode = 'stereo') {
+        const left = computeAspectMapping(
+            getRgbEyeSourceRect(sourceWidth, sourceHeight, 'left', captureMode), fitMode);
         return {
-            left: computeAspectMapping(
-                getRgbEyeSourceRect(sourceWidth, sourceHeight, 'left'), fitMode),
-            right: computeAspectMapping(
-                getRgbEyeSourceRect(sourceWidth, sourceHeight, 'right'), fitMode),
+            left,
+            right: captureMode === 'mono' ? null : computeAspectMapping(
+                getRgbEyeSourceRect(sourceWidth, sourceHeight, 'right', captureMode), fitMode),
+            captureMode,
+            resolvedLayout: getRgbLayout(sourceWidth, sourceHeight, captureMode),
         };
     }
 
@@ -428,8 +489,10 @@
     }
 
     function drawRgbPair(ctx, image, mappings) {
-        drawRgbEye(ctx, image, mappings.left, 0);
-        drawRgbEye(ctx, image, mappings.right, EYE_SIZE);
+        ctx.fillStyle = '#000';
+        ctx.fillRect(0, 0, OUTPUT_SIZE, EYE_SIZE);
+        if (mappings.left) drawRgbEye(ctx, image, mappings.left, 0);
+        if (mappings.right) drawRgbEye(ctx, image, mappings.right, EYE_SIZE);
     }
 
     function mapStoredPixelToViewUv(x, y, size, mapping) {
@@ -568,10 +631,12 @@
         previewCtx.fillStyle = '#000';
         previewCtx.fillRect(0, 0, OUTPUT_SIZE, OUTPUT_SIZE);
         const options = getEncodingOptions();
+        const captureMode = captureSetReady(state.latestDepth) ? state.latestDepth.mode : 'stereo';
         if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
             const dimensions = getImageDimensions(video);
             if (dimensions.width && dimensions.height) {
-                const mappings = buildRgbMappings(dimensions.width, dimensions.height, options.fitMode);
+                const mappings = buildRgbMappings(
+                    dimensions.width, dimensions.height, options.fitMode, captureMode);
                 drawRgbPair(previewCtx, video, mappings);
             }
         }
@@ -579,17 +644,18 @@
         if (state.encodedDepth.left) {
             previewCtx.drawImage(state.encodedDepth.left.canvas, 0, EYE_SIZE);
         } else {
-            drawWaitingDepth(previewCtx, 0, 'WAITING FOR LEFT WEBXR DEPTH');
+            drawWaitingDepth(previewCtx, 0,
+                captureMode === 'mono' ? 'WAITING FOR MONO WEBXR DEPTH' : 'WAITING FOR LEFT WEBXR DEPTH');
         }
         if (state.encodedDepth.right) {
             previewCtx.drawImage(state.encodedDepth.right.canvas, EYE_SIZE, EYE_SIZE);
-        } else {
+        } else if (captureMode === 'stereo') {
             drawWaitingDepth(previewCtx, EYE_SIZE, 'WAITING FOR RIGHT WEBXR DEPTH');
         }
         state.previewRaf = requestAnimationFrame(renderVisiblePreview);
     }
 
-    function copyDepthInformation(depthInfo, view, viewIndex, viewerPose) {
+    function copyDepthInformation(depthInfo, view, viewArrayIndex, viewerPose) {
         const format = state.session.depthDataFormat;
         const copiedBuffer = depthInfo.data.slice(0);
         const data = format === 'float32'
@@ -622,8 +688,12 @@
         const captureLocalFromSensor = mat4Multiply(viewerFromReference, referenceFromSensor);
         const sensorFromCaptureLocal = mat4Invert(captureLocalFromSensor);
         if (!sensorFromCaptureLocal) throw new Error('The depth sensor transform is not invertible.');
+        const captureLocalFromView = mat4Multiply(viewerFromReference, viewTransform);
+        const viewFromCaptureLocal = mat4Invert(captureLocalFromView);
+        if (!viewFromCaptureLocal) throw new Error('The XR view transform is not invertible.');
         const projectionInverse = mat4Invert(sensorProjection);
         if (!projectionInverse) throw new Error('The depth sensor projection matrix is not invertible.');
+        const viewProjectionInverse = mat4Invert(viewProjection);
         const normDepthBufferFromNormView = matrixFrom(
             depthInfo.normDepthBufferFromNormView.matrix, mat4Identity());
         const normViewFromNormDepthBuffer = mat4Invert(normDepthBufferFromNormView);
@@ -632,8 +702,11 @@
         }
 
         return {
-            eye: view.eye || (viewIndex === 0 ? 'left' : 'right'),
-            viewIndex,
+            eye: view.eye === 'left' || view.eye === 'right' || view.eye === 'none'
+                ? view.eye : 'none',
+            viewArrayIndex,
+            xrViewIndex: Number.isInteger(view.index) ? view.index : viewArrayIndex,
+            isFirstPersonObserver: !!view.isFirstPersonObserver,
             width: depthInfo.width,
             height: depthInfo.height,
             format,
@@ -646,7 +719,9 @@
             captureLocalFromSensor,
             sensorFromCaptureLocal,
             viewProjectionMatrix: viewProjection,
-            captureLocalFromView: mat4Multiply(viewerFromReference, viewTransform),
+            viewProjectionMatrixInverse: viewProjectionInverse,
+            captureLocalFromView,
+            viewFromCaptureLocal,
             projectionSource: depthProjectionAvailable ? 'XRDepthInformation.projectionMatrix' : 'XRView.projectionMatrix fallback',
             transformSource: depthTransformAvailable ? 'XRDepthInformation.transform' : 'XRView.transform fallback',
             viewport: viewport ? {
@@ -658,32 +733,80 @@
         };
     }
 
-    function assignEyeKey(view, index) {
-        if (view.eye === 'right') return 'right';
-        if (view.eye === 'left') return 'left';
-        return index === 0 ? 'left' : 'right';
+    function collectDepthCaptureSet(frame, pose) {
+        const primary = Array.from(pose.views, (view, index) => ({ view, index }))
+            .filter(({ view }) => !view.isFirstPersonObserver);
+        if (primary.length < 1) throw new Error('The XR runtime exposed no primary view.');
+        if (primary.length > MAX_CAPTURE_VIEWS) {
+            throw new Error(
+                `This fixed 2-slot format supports one mono view or one left/right pair; the runtime exposed ${primary.length}. No view was discarded.`);
+        }
+
+        let ordered;
+        let mode;
+        if (primary.length === 1) {
+            ordered = primary;
+            mode = 'mono';
+        } else {
+            const left = primary.find((entry) => entry.view.eye === 'left');
+            const right = primary.find((entry) => entry.view.eye === 'right');
+            if (!left || !right) {
+                throw new Error('A two-view capture must contain one left-eye and one right-eye XRView.');
+            }
+            ordered = [left, right];
+            mode = 'stereo';
+        }
+
+        const records = [];
+        const missing = [];
+        for (let slot = 0; slot < ordered.length; slot += 1) {
+            const { view, index } = ordered[slot];
+            const depthInfo = frame.getDepthInformation(view);
+            if (!depthInfo) {
+                missing.push({ slot, eye: view.eye || 'none', viewArrayIndex: index });
+                records.push(null);
+                continue;
+            }
+            const record = copyDepthInformation(depthInfo, view, index, pose);
+            record.outputSlot = slot;
+            record.viewId = mode === 'mono' ? 'mono' : record.eye;
+            records.push(record);
+        }
+
+        return {
+            mode,
+            viewCount: ordered.length,
+            eyes: ordered.map(({ view }) => view.eye || 'none'),
+            views: records.filter(Boolean),
+            left: records[0] || null,
+            right: mode === 'stereo' ? (records[1] || null) : null,
+            missing,
+        };
     }
 
-    function currentPreviewMappings(options) {
+    function currentPreviewMappings(options, capture = state.latestDepth) {
         const dimensions = getImageDimensions(video);
-        if (!dimensions.width || !dimensions.height) return null;
-        return buildRgbMappings(dimensions.width, dimensions.height, options.fitMode);
+        if (!dimensions.width || !dimensions.height || !captureSetReady(capture)) return null;
+        return buildRgbMappings(
+            dimensions.width, dimensions.height, options.fitMode, capture.mode);
     }
 
-    function updateDepthPreviewAsync(pair) {
+    function updateDepthPreviewAsync(capture) {
         const generation = ++state.pendingEncodeGeneration;
         const options = getEncodingOptions();
-        const mappings = currentPreviewMappings(options);
+        const mappings = currentPreviewMappings(options, capture);
         if (!mappings) return;
         setTimeout(() => {
             if (!state.running || generation !== state.pendingEncodeGeneration) return;
             try {
-                const left = encodeDepthAtlas(pair.left, options, mappings.left);
-                const right = encodeDepthAtlas(pair.right, options, mappings.right);
+                const left = encodeDepthAtlas(capture.left, options, mappings.left);
+                const right = capture.right && mappings.right
+                    ? encodeDepthAtlas(capture.right, options, mappings.right)
+                    : null;
                 if (!state.running || generation !== state.pendingEncodeGeneration) return;
                 state.encodedDepth.left = left;
                 state.encodedDepth.right = right;
-                snapBtn.disabled = false;
+                snapBtn.disabled = !captureSetReady(state.latestDepth);
             } catch (error) {
                 console.error(error);
                 setStatus(`Depth preview encoding failed: ${error.message}`, 'error');
@@ -769,23 +892,26 @@
         });
     }
 
-    function composeCapture(ctx, rgbImage, pair, options) {
+    function composeCapture(ctx, rgbImage, capture, options) {
+        if (!captureSetReady(capture)) throw new Error('No complete mono/stereo XR depth capture is available.');
         const dimensions = getImageDimensions(rgbImage);
         if (!dimensions.width || !dimensions.height) {
             throw new Error('Captured RGB frame has invalid dimensions.');
         }
-        const mappings = buildRgbMappings(dimensions.width, dimensions.height, options.fitMode);
+        const mappings = buildRgbMappings(
+            dimensions.width, dimensions.height, options.fitMode, capture.mode);
 
         ctx.clearRect(0, 0, OUTPUT_SIZE, OUTPUT_SIZE);
         ctx.fillStyle = '#000';
         ctx.fillRect(0, 0, OUTPUT_SIZE, OUTPUT_SIZE);
         drawRgbPair(ctx, rgbImage, mappings);
 
-        const leftAtlas = encodeDepthAtlas(pair.left, options, mappings.left);
-        const rightAtlas = encodeDepthAtlas(pair.right, options, mappings.right);
-        // putImageData writes exact RGBA bytes. Alpha stores validity/clipping state.
+        const leftAtlas = encodeDepthAtlas(capture.left, options, mappings.left);
+        const rightAtlas = capture.right && mappings.right
+            ? encodeDepthAtlas(capture.right, options, mappings.right)
+            : null;
         ctx.putImageData(leftAtlas.imageData, 0, EYE_SIZE);
-        ctx.putImageData(rightAtlas.imageData, EYE_SIZE, EYE_SIZE);
+        if (rightAtlas) ctx.putImageData(rightAtlas.imageData, EYE_SIZE, EYE_SIZE);
         return { leftAtlas, rightAtlas, mappings, dimensions };
     }
 
@@ -975,6 +1101,7 @@
     }
 
     function summarizeMapping(mapping) {
+        if (!mapping) return null;
         return {
             fitMode: mapping.fitMode,
             sourceRectPixels: mapping.sourceRectPixels,
@@ -986,10 +1113,29 @@
         };
     }
 
-    function summarizeDepth(depth, payloadDescriptor, mapping) {
+    function payloadPlanForCapture(capture) {
+        if (capture.mode === 'mono') {
+            return [{ viewId: 'mono', depth: capture.left, chunkType: 'mdPN' }];
+        }
+        return [
+            { viewId: 'left', depth: capture.left, chunkType: 'mdPL' },
+            { viewId: 'right', depth: capture.right, chunkType: 'mdPR' },
+        ];
+    }
+
+    async function buildCapturePayloadInfos(capture) {
+        return Promise.all(payloadPlanForCapture(capture).map(async (plan) => ({
+            ...plan,
+            ...(await buildDepthPayloadInfo(plan.depth, plan.chunkType)),
+        })));
+    }
+
+    function summarizeDepth(depth, viewId, payloadDescriptor, mapping) {
         return {
+            viewId,
             eye: depth.eye,
-            viewIndex: depth.viewIndex,
+            viewArrayIndex: depth.viewArrayIndex,
+            xrViewIndex: depth.xrViewIndex,
             nativeBuffer: {
                 width: depth.width,
                 height: depth.height,
@@ -1006,36 +1152,49 @@
                 xDirection: 'right',
                 yDirection: 'down',
                 normDepthBufferFromNormView: Array.from(depth.normDepthBufferFromNormView),
-                normViewFromNormDepthBuffer: depth.normViewFromNormDepthBuffer
-                    ? Array.from(depth.normViewFromNormDepthBuffer) : null,
+                normViewFromNormDepthBuffer: Array.from(depth.normViewFromNormDepthBuffer),
+            },
+            xrViewGeometry: {
+                projectionMatrix: Array.from(depth.viewProjectionMatrix),
+                projectionMatrixInverse: depth.viewProjectionMatrixInverse
+                    ? Array.from(depth.viewProjectionMatrixInverse) : null,
+                captureLocalFromView: Array.from(depth.captureLocalFromView),
+                viewFromCaptureLocal: Array.from(depth.viewFromCaptureLocal),
+                viewport: depth.viewport,
             },
             sensorGeometry: {
                 projectionMatrix: Array.from(depth.sensorProjectionMatrix),
-                projectionMatrixInverse: depth.sensorProjectionMatrixInverse
-                    ? Array.from(depth.sensorProjectionMatrixInverse) : null,
+                projectionMatrixInverse: Array.from(depth.sensorProjectionMatrixInverse),
                 captureLocalFromSensor: Array.from(depth.captureLocalFromSensor),
                 sensorFromCaptureLocal: Array.from(depth.sensorFromCaptureLocal),
                 projectionSource: depth.projectionSource,
                 transformSource: depth.transformSource,
                 depthMeaning: 'camera-plane-z-from-sensor-near-plane',
-                viewport: depth.viewport,
             },
             rgbAndAtlasMapping: summarizeMapping(mapping),
         };
     }
 
     function makeMetadata({
-        pair, options, composed, rgbCapture, xrTiming, captureId,
-        imageFilename, jsonFilename, leftPayloadInfo, rightPayloadInfo,
+        capture, options, composed, rgbCapture, xrTiming, captureId,
+        imageFilename, jsonFilename, payloadInfos,
     }) {
-        const rgbLayout = getRgbLayout(composed.dimensions.width, composed.dimensions.height);
         const phaseNominalStep = options.maxDistance / (options.cycles * 255);
         const estimatedSkew = rgbCapture.timing && Number.isFinite(rgbCapture.timing.frameCallbackAtPerformanceMs)
             ? xrTiming.xrFrameTimePerformanceMs - rgbCapture.timing.frameCallbackAtPerformanceMs
             : null;
+        const payloadById = Object.fromEntries(payloadInfos.map((info) => [info.viewId, info]));
+        const depthViews = capture.views.map((depth) => {
+            const viewId = capture.mode === 'mono' ? 'mono' : depth.eye;
+            const mapping = viewId === 'right' ? composed.mappings.right : composed.mappings.left;
+            return summarizeDepth(depth, viewId, payloadById[viewId].descriptor, mapping);
+        });
+        const depthById = Object.fromEntries(depthViews.map((view) => [view.viewId, view]));
+        const baselines = computeCaptureBaselines(capture);
+        const trackCount = state.stream ? state.stream.getVideoTracks().length : 1;
 
         return {
-            schema: 'mr-phase-shift-snapshot/v3',
+            schema: 'mr-phase-shift-snapshot/v4',
             captureId,
             capturedAt: new Date().toISOString(),
             files: {
@@ -1061,12 +1220,31 @@
                 clipSpace: 'WebGL NDC x/y/z in [-1,+1]',
             },
             source: {
+                xrSessionMode: 'immersive-ar',
                 rgbApi: 'MediaDevices.getUserMedia',
                 depthApi: 'WebXR Depth Sensing Module XRFrame.getDepthInformation',
                 referenceSpaceType: xrTiming.referenceSpaceType,
             },
+            captureTrigger: xrTiming.captureTrigger || null,
+            viewConfiguration: {
+                mode: capture.mode,
+                primaryViewCount: capture.viewCount,
+                eyeOrder: capture.views.map((view) => view.eye),
+                viewIds: depthViews.map((view) => view.viewId),
+                supportsPlaybackOnDifferentViewConfiguration: true,
+                stereoBaseline: baselines ? {
+                    ...baselines,
+                    ipdEquivalentMeters: baselines.viewOriginDistanceMeters,
+                    ipdEquivalentSource: 'Euclidean distance between left and right XRView origins in capture-local coordinates',
+                    depthCameraBaselineMeters: baselines.depthSensorOriginDistanceMeters,
+                    isGuaranteedBiologicalInterpupillaryDistance: false,
+                    isGuaranteedPhysicalRgbCameraSeparation: false,
+                    mayBeAnonymizedOrQuantizedByUserAgent: true,
+                    useAsPlaybackEyeSeparation: false,
+                } : null,
+            },
             timing: {
-                synchronizationMethod: 'next getUserMedia video frame, followed by next available WebXR depth frame',
+                synchronizationMethod: 'next getUserMedia video frame, followed by next complete WebXR depth view set',
                 hardwareSynchronized: false,
                 video: rgbCapture.timing,
                 xrFrameTimePerformanceMs: xrTiming.xrFrameTimePerformanceMs,
@@ -1075,22 +1253,41 @@
             output: {
                 width: OUTPUT_SIZE,
                 height: OUTPUT_SIZE,
-                topLeft: { role: 'left-rgb', x: 0, y: 0, width: EYE_SIZE, height: EYE_SIZE },
-                topRight: { role: 'right-rgb', x: EYE_SIZE, y: 0, width: EYE_SIZE, height: EYE_SIZE },
-                bottomLeft: { role: 'left-depth-atlas', x: 0, y: EYE_SIZE, width: EYE_SIZE, height: EYE_SIZE },
-                bottomRight: { role: 'right-depth-atlas', x: EYE_SIZE, y: EYE_SIZE, width: EYE_SIZE, height: EYE_SIZE },
+                topLeft: {
+                    role: capture.mode === 'mono' ? 'mono-rgb' : 'left-rgb',
+                    viewId: capture.mode === 'mono' ? 'mono' : 'left',
+                    x: 0, y: 0, width: EYE_SIZE, height: EYE_SIZE,
+                },
+                topRight: capture.mode === 'mono'
+                    ? { role: 'unused', x: EYE_SIZE, y: 0, width: EYE_SIZE, height: EYE_SIZE }
+                    : { role: 'right-rgb', viewId: 'right', x: EYE_SIZE, y: 0, width: EYE_SIZE, height: EYE_SIZE },
+                bottomLeft: {
+                    role: capture.mode === 'mono' ? 'mono-depth-atlas' : 'left-depth-atlas',
+                    viewId: capture.mode === 'mono' ? 'mono' : 'left',
+                    x: 0, y: EYE_SIZE, width: EYE_SIZE, height: EYE_SIZE,
+                },
+                bottomRight: capture.mode === 'mono'
+                    ? { role: 'unused', x: EYE_SIZE, y: EYE_SIZE, width: EYE_SIZE, height: EYE_SIZE }
+                    : { role: 'right-depth-atlas', viewId: 'right', x: EYE_SIZE, y: EYE_SIZE, width: EYE_SIZE, height: EYE_SIZE },
                 depthTileSize: TILE_SIZE,
             },
             rgb: {
                 sourceWidth: composed.dimensions.width,
                 sourceHeight: composed.dimensions.height,
                 sourceAspect: composed.dimensions.width / composed.dimensions.height,
+                mediaStreamVideoTrackCount: trackCount,
+                physicalCameraCount: null,
+                physicalCameraCountReason: 'getUserMedia exposes MediaStreamTracks, not a calibrated count or baseline for internal physical cameras.',
                 selectedLayout: rgbLayoutSelect.value,
-                resolvedLayout: rgbLayout,
+                resolvedLayout: composed.mappings.resolvedLayout,
                 fitMode: options.fitMode,
                 trackSettings: rgbCapture.trackSettings,
-                left: summarizeMapping(composed.mappings.left),
-                right: summarizeMapping(composed.mappings.right),
+                mappings: capture.mode === 'mono'
+                    ? { mono: summarizeMapping(composed.mappings.left) }
+                    : {
+                        left: summarizeMapping(composed.mappings.left),
+                        right: summarizeMapping(composed.mappings.right),
+                    },
                 calibrationSource: 'corresponding depth.sensorGeometry by explicit assumption',
             },
             depthSession: xrTiming.depthSessionInfo,
@@ -1098,8 +1295,12 @@
                 preferredSource: 'embedded-native-webxr-depth-chunks',
                 exactDeviceSamplesPreserved: true,
                 conversion: 'zMeters = rawSample * rawValueToMeters; rawSample == 0 means invalid',
-                leftChunkType: leftPayloadInfo.descriptor.chunkType,
-                rightChunkType: rightPayloadInfo.descriptor.chunkType,
+                views: payloadInfos.map((info) => ({
+                    viewId: info.viewId,
+                    eye: info.depth.eye,
+                    xrViewIndex: info.depth.xrViewIndex,
+                    chunkType: info.descriptor.chunkType,
+                })),
             },
             phaseDepthFallback: {
                 purpose: 'visual/video-friendly fallback; embedded native depth is authoritative for metric reconstruction',
@@ -1120,13 +1321,6 @@
                     128: 'valid sample beyond maxDistanceMeters; value is lower-bounded, not exact',
                     255: 'valid sample within encoding range',
                 },
-                decoder: [
-                    'c=coarse/255; ci=inverse/255; s=2*sine/255-1; q=2*cosine/255-1',
-                    'cleanCoarse=clamp(0.5*(c + (1-ci)),0,1)',
-                    'phase01=atan2(s,q)/(2*pi); if phase01<0 then phase01+=1',
-                    'cycle=round(cleanCoarse*phaseCycles - phase01)',
-                    'decodedZNorm=clamp((cycle+phase01)/phaseCycles,0,1)',
-                ],
             },
             reconstruction: {
                 preferredInput: 'metricDepth embedded native chunks',
@@ -1140,7 +1334,13 @@
                     'sensorPoint=ray*(zMeters/(-ray.z))',
                 ],
                 sensorPointToCaptureLocal: 'captureLocalPoint=captureLocalFromSensor*[sensorPoint,1]',
-                renderOnPlaybackDevice: 'apply the playback camera view/projection to captureLocalPoint; do not substitute playback intrinsics during unprojection',
+                attachAtPlaybackViewer: 'referenceFromCaptureLocal = playbackViewerPose.transform.matrix',
+                renderForEachPlaybackView: [
+                    'playbackViewFromCaptureLocal = playbackXRView.transform.inverse.matrix * referenceFromCaptureLocal',
+                    'clip = playbackXRView.projectionMatrix * playbackViewFromCaptureLocal * captureLocalPoint',
+                ],
+                playbackIpdRule: 'Use every playback XRView transform exactly as supplied by the playback runtime; never rescale geometry by capture IPD.',
+                captureBaselinePurpose: 'Calibration, stereo fusion, validation, and provenance only.',
                 physicalScale: 'one coordinate unit is one meter',
             },
             occlusion: {
@@ -1148,71 +1348,66 @@
                 invalidSamplesCreateHoles: true,
                 clippedPhaseSamplesAreNotExactOccluders: true,
                 embeddedNativeDepthRecommended: true,
-                comparison: 'discard a rendered fragment when its camera-plane z is farther than the reconstructed captured surface z plus tolerance, after both are in the same camera-local frame',
                 recommendedStartingToleranceMeters: 0.01,
             },
             depth: {
-                left: summarizeDepth(pair.left, leftPayloadInfo.descriptor, composed.mappings.left),
-                right: summarizeDepth(pair.right, rightPayloadInfo.descriptor, composed.mappings.right),
-            },
-            atlasStatistics: {
-                left: composed.leftAtlas.stats,
-                right: composed.rightAtlas.stats,
+                views: depthViews,
+                byId: depthById,
             },
+            atlasStatistics: capture.mode === 'mono'
+                ? { mono: composed.leftAtlas.stats }
+                : { left: composed.leftAtlas.stats, right: composed.rightAtlas.stats },
         };
     }
 
-    async function buildSelfContainedPng(basePngBlob, metadata, leftPayloadInfo, rightPayloadInfo) {
+    async function buildSelfContainedPng(basePngBlob, metadata, payloadInfos) {
         const baseBytes = new Uint8Array(await basePngBlob.arrayBuffer());
         const metadataChunk = createPngChunk(
             'iTXt', createITXtData(METADATA_KEYWORD, JSON.stringify(metadata)));
-        const leftChunk = createPngChunk(leftPayloadInfo.descriptor.chunkType, leftPayloadInfo.payload);
-        const rightChunk = createPngChunk(rightPayloadInfo.descriptor.chunkType, rightPayloadInfo.payload);
-        const bytes = insertPngChunksBeforeIend(baseBytes, [metadataChunk, leftChunk, rightChunk]);
+        const depthChunks = payloadInfos.map((info) =>
+            createPngChunk(info.descriptor.chunkType, info.payload));
+        const bytes = insertPngChunksBeforeIend(baseBytes, [metadataChunk, ...depthChunks]);
         return new Blob([bytes], { type: 'image/png' });
     }
 
-    async function exportSnapshot(pair, rgbCapture, xrTiming) {
+    async function exportSnapshot(capture, rgbCapture, xrTiming) {
         snapBtn.disabled = true;
-        setStatus('Encoding calibrated RGB-D, native metric depth, and camera-local reconstruction metadata…');
+        setStatus('Encoding adaptive calibrated RGB-D, native metric depth, and camera-local reconstruction metadata…');
         const options = getEncodingOptions();
 
         try {
-            const composed = composeCapture(captureCtx, rgbCapture.image, pair, options);
+            const composed = composeCapture(captureCtx, rgbCapture.image, capture, options);
             state.encodedDepth.left = composed.leftAtlas;
             state.encodedDepth.right = composed.rightAtlas;
-
-            const [leftPayloadInfo, rightPayloadInfo] = await Promise.all([
-                buildDepthPayloadInfo(pair.left, 'mdPL'),
-                buildDepthPayloadInfo(pair.right, 'mdPR'),
-            ]);
+            const payloadInfos = await buildCapturePayloadInfos(capture);
 
             const captureId = createCaptureId();
             const stamp = timestampForFilename();
-            const baseName = `MRPS_v3_${stamp}_${captureId.slice(0, 8)}`;
+            const baseName = `MRPS_v4_${capture.mode}_${stamp}_${captureId.slice(0, 8)}`;
             const imageFilename = `${baseName}.png`;
             const jsonFilename = `${baseName}.json`;
             const metadata = makeMetadata({
-                pair, options, composed, rgbCapture, xrTiming, captureId,
-                imageFilename, jsonFilename, leftPayloadInfo, rightPayloadInfo,
+                capture, options, composed, rgbCapture, xrTiming, captureId,
+                imageFilename, jsonFilename, payloadInfos,
             });
 
             const basePngBlob = await canvasToBlob(captureCanvas, 'image/png');
             const selfContainedPng = await buildSelfContainedPng(
-                basePngBlob, metadata, leftPayloadInfo, rightPayloadInfo);
+                basePngBlob, metadata, payloadInfos);
             downloadBlob(selfContainedPng, imageFilename);
-
-            const jsonBlob = new Blob([JSON.stringify(metadata, null, 2)], {
+            downloadBlob(new Blob([JSON.stringify(metadata, null, 2)], {
                 type: 'application/json',
-            });
-            downloadBlob(jsonBlob, jsonFilename);
+            }), jsonFilename);
 
             const skew = metadata.timing.estimatedVideoToDepthSkewMs;
+            const baseline = metadata.viewConfiguration.stereoBaseline;
+            const depthSummary = capture.views
+                .map((depth) => `${depth.viewId}/${depth.eye} ${depth.width}×${depth.height}`)
+                .join(', ');
             setStatus(
-                `Saved self-contained metric RGB-D PNG + JSON. ` +
-                `Native depth L ${pair.left.width}×${pair.left.height}, ` +
-                `R ${pair.right.width}×${pair.right.height}; ` +
-                `camera-local origin, ${Number.isFinite(skew) ? `estimated frame skew ${skew.toFixed(2)} ms` : 'frame skew unavailable'}.`,
+                `Saved ${capture.mode} metric RGB-D PNG + JSON. ${depthSummary}; ` +
+                `${baseline ? `capture view baseline ${(baseline.viewOriginDistanceMeters * 1000).toFixed(2)} mm; ` : ''}` +
+                `${Number.isFinite(skew) ? `estimated frame skew ${skew.toFixed(2)} ms` : 'frame skew unavailable'}.`,
                 'ok');
         } catch (error) {
             console.error(error);
@@ -1223,7 +1418,8 @@
                 rgbCapture.image.close();
             }
             state.pendingRgbCapture = null;
-            snapBtn.disabled = !(state.latestDepth.left && state.latestDepth.right);
+            state.pendingTrigger = null;
+            snapBtn.disabled = !captureSetReady(state.latestDepth);
         }
     }
 
@@ -1246,30 +1442,28 @@
             (time - state.lastDepthCopyAt >= DEPTH_PREVIEW_INTERVAL_MS);
         if (!shouldCopy) return;
 
-        const currentPair = { left: null, right: null };
+        let currentCapture;
         try {
-            pose.views.forEach((view, index) => {
-                const depthInfo = frame.getDepthInformation(view);
-                if (!depthInfo) return;
-                const key = assignEyeKey(view, index);
-                currentPair[key] = copyDepthInformation(depthInfo, view, index, pose);
-            });
+            currentCapture = collectDepthCaptureSet(frame, pose);
         } catch (error) {
             console.error(error);
-            setStatus(`WebXR depth read failed: ${error.name || 'Error'}: ${error.message}`, 'error');
+            setStatus(`WebXR view/depth read failed: ${error.name || 'Error'}: ${error.message}`, 'error');
             return;
         }
 
-        if (!currentPair.left || !currentPair.right) {
+        if (!captureSetReady(currentCapture)) {
+            const missing = currentCapture.missing
+                .map((entry) => `slot ${entry.slot} (${entry.eye})`)
+                .join(', ');
             setStatus(
-                `The XR runtime returned ${currentPair.left ? 'left' : currentPair.right ? 'right' : 'no'} ` +
-                'depth only. Waiting for real left and right depth; no simulated copy will be written.',
+                `Waiting for real depth for the complete ${currentCapture.mode} XR view set` +
+                `${missing ? `; missing ${missing}` : ''}. No view is copied or fabricated.`,
                 'error');
             return;
         }
 
         state.lastDepthCopyAt = time;
-        state.latestDepth = currentPair;
+        state.latestDepth = currentCapture;
         state.frameCounter += 1;
 
         const snapshotNow = state.snapshotRequested && state.pendingRgbCapture;
@@ -1279,19 +1473,22 @@
             state.pendingRgbCapture = null;
             const captureSessionInfo = clonePlain(state.depthSessionInfo);
             const captureReferenceSpaceType = state.referenceSpaceType;
-            setTimeout(() => exportSnapshot(currentPair, rgbCapture, {
+            const captureTrigger = clonePlain(state.pendingTrigger);
+            setTimeout(() => exportSnapshot(currentCapture, rgbCapture, {
                 xrFrameTimePerformanceMs: time,
                 depthSessionInfo: captureSessionInfo,
                 referenceSpaceType: captureReferenceSpaceType,
+                captureTrigger,
             }), 0);
         } else {
-            updateDepthPreviewAsync(currentPair);
+            updateDepthPreviewAsync(currentCapture);
             if (state.frameCounter === 1) {
+                const depthSummary = currentCapture.views
+                    .map((depth) => `${depth.viewId}/${depth.eye} ${depth.width}×${depth.height}`)
+                    .join(', ');
                 setStatus(
-                    `Real device active. Camera ${video.videoWidth}×${video.videoHeight}; ` +
-                    `WebXR depth ${state.session.depthUsage}/${state.session.depthDataFormat}; ` +
-                    `L ${currentPair.left.width}×${currentPair.left.height}, ` +
-                    `R ${currentPair.right.width}×${currentPair.right.height}.`,
+                    `Real ${currentCapture.mode} device active. Camera ${video.videoWidth}×${video.videoHeight}; ` +
+                    `WebXR depth ${state.session.depthUsage}/${state.session.depthDataFormat}; ${depthSummary}.`,
                     'ok');
             }
         }
@@ -1355,16 +1552,20 @@
         state.referenceSpaceType = referenceSpaceType;
 
         session.addEventListener('select', requestSnapshot);
+        document.getElementById('panel').addEventListener('beforexrselect', (event) => {
+            event.preventDefault();
+        });
         session.addEventListener('end', () => {
             if (!state.stopping) cleanupAfterStop(true);
         }, { once: true });
     }
 
-    async function requestSnapshot() {
+    async function requestSnapshot(event) {
         if (!state.running || state.snapshotArming || state.snapshotRequested) return;
         state.snapshotArming = true;
+        state.pendingTrigger = describeCaptureTrigger(event);
         snapBtn.disabled = true;
-        setStatus('Capturing the next RGB frame, then pairing it with the next WebXR depth frame…');
+        setStatus('Capturing the next RGB frame, then pairing it with the next complete WebXR depth view set…');
         try {
             const rgbCapture = await captureNextVideoFrame();
             if (!state.running) {
@@ -1373,11 +1574,12 @@
             }
             state.pendingRgbCapture = rgbCapture;
             state.snapshotRequested = true;
-            setStatus('RGB frame captured; waiting for the next real stereo WebXR depth frame…');
+            setStatus('RGB frame captured; waiting for the next real mono/stereo WebXR depth set…');
         } catch (error) {
             console.error(error);
+            state.pendingTrigger = null;
             setStatus(`RGB frame capture failed: ${error.message}`, 'error');
-            snapBtn.disabled = !(state.latestDepth.left && state.latestDepth.right);
+            snapBtn.disabled = !captureSetReady(state.latestDepth);
         } finally {
             state.snapshotArming = false;
         }
@@ -1414,7 +1616,7 @@
                 optionalFeatures: ['dom-overlay'],
                 depthSensing: {
                     usagePreference: ['cpu-optimized'],
-                    dataFormatPreference: ['luminance-alpha', 'float32'],
+                    dataFormatPreference: ['float32', 'unsigned-short', 'luminance-alpha'],
                     depthTypeRequest: ['raw', 'smooth'],
                     matchDepthView: true,
                 },
@@ -1453,7 +1655,7 @@
             state.running = true;
             state.lastDepthCopyAt = -Infinity;
             state.frameCounter = 0;
-            state.latestDepth = { left: null, right: null };
+            state.latestDepth = null;
             state.encodedDepth = { left: null, right: null };
             snapBtn.disabled = true;
             stopBtn.disabled = false;
@@ -1463,7 +1665,7 @@
             setStatus(
                 `Camera opened at ${video.videoWidth}×${video.videoHeight}` +
                 `${trackSettings.frameRate ? ` @ ${trackSettings.frameRate} fps` : ''}. ` +
-                'Waiting for real left/right XR depth frames…');
+                'Waiting for a complete real mono or left/right stereo XR depth set…');
         } catch (error) {
             console.error(error);
             await cleanupAfterStop(false);
@@ -1490,6 +1692,7 @@
             state.pendingRgbCapture.image.close();
         }
         state.pendingRgbCapture = null;
+        state.pendingTrigger = null;
 
         const session = state.session;
         state.session = null;
@@ -1506,7 +1709,7 @@
         state.referenceSpaceType = null;
         state.gl = null;
         state.baseLayer = null;
-        state.latestDepth = { left: null, right: null };
+        state.latestDepth = null;
         state.encodedDepth = { left: null, right: null };
         state.cameraSettings = null;
         state.depthSessionInfo = null;
@@ -1527,7 +1730,7 @@
     snapBtn.addEventListener('click', requestSnapshot);
     stopBtn.addEventListener('click', stopRealDevice);
     const refreshDepthPreview = () => {
-        if (state.latestDepth.left && state.latestDepth.right) updateDepthPreviewAsync(state.latestDepth);
+        if (captureSetReady(state.latestDepth)) updateDepthPreviewAsync(state.latestDepth);
     };
     rgbLayoutSelect.addEventListener('change', refreshDepthPreview);
     fitModeSelect.addEventListener('change', refreshDepthPreview);
@@ -1542,6 +1745,9 @@
         mat4Identity,
         mat4Multiply,
         mat4Invert,
+        matrixOrigin,
+        computeCaptureBaselines,
+        captureSetReady,
         computeAspectMapping,
         buildRgbMappings,
         mapStoredPixelToViewUv,
@@ -1549,6 +1755,7 @@
         composeCapture,
         createDepthPayload,
         buildDepthPayloadInfo,
+        buildCapturePayloadInfos,
         makeMetadata,
         canvasToBlob,
         buildSelfContainedPng,
