Skip to content

Fix Xcode 26 C++ build + iPhone 17 Pro depth visualization corruption#57

Open
wonmor wants to merge 4 commits into
StandardCyborg:masterfrom
wonmor:master
Open

Fix Xcode 26 C++ build + iPhone 17 Pro depth visualization corruption#57
wonmor wants to merge 4 commits into
StandardCyborg:masterfrom
wonmor:master

Conversation

@wonmor
Copy link
Copy Markdown

@wonmor wonmor commented May 7, 2026

Summary

Two independent fixes that affect anyone shipping a TrueDepth scanning app on current Apple hardware/toolchains:

  1. C++ compat for Xcode 26 + static linking (671a57e) — addresses build failures in Xcode 26 with the modern C++ toolchain, and switches some targets to static linking to play nicer with downstream Swift apps.

  2. iPhone 17 Pro depth visualization corruption (416bcfa) — the depth CVPixelBuffer-backed MTLTexture in DepthColoringFilter._metalTexture(fromDepthBuffer:) was created with MTLTextureUsage.shaderWrite, but the texture is read (it's the source of an MPSImageGaussianBlur, never a destination). The A17 Pro (iPhone 15 Pro Max) silently tolerated the wrong flag; the A19 GPU on iPhone 17 Pro enforces it strictly and produces blocky purple/blue artifacts on the depth-coloring overlay during scanning. One-line fix: .shaderWrite.shaderRead.

Test plan

  • Builds cleanly in Xcode 26
  • Depth-coloring overlay renders correctly on iPhone 17 Pro front-facing TrueDepth scan
  • No regression on iPhone 15 Pro Max
  • Verify on iPhone 16 Pro / 14 Pro family (older A-series) — expected unaffected

🤖 Generated with Claude Code

Depth CVPixelBuffer-backed Metal texture was created with
MTLTextureUsage.shaderWrite but is actually used as the source of
an MPSImageGaussianBlur (read-only). The A19 GPU on iPhone 17 Pro
enforces this strictly, producing blocky purple/blue artifacts on
the depth-coloring overlay; the A17 Pro silently tolerated the
wrong flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aaptho
Copy link
Copy Markdown
Collaborator

aaptho commented May 8, 2026

Thanks for the fix! I don’t see your first commit in here, does the build work for you without it?

wonmor and others added 3 commits May 14, 2026 06:25
Upstream's commit 8c3bc69 picked between two orientation matrices
based on whether intrinsicMatrixReferenceDimensions reports a widescreen
sensor (>1.5 aspect ratio). On real-world iPhone 15 Pro Max devices,
that heuristic mis-classified the sensor as widescreen at runtime and
flipped the scan orientation.

Switch to a hardware-id check via uname(): apply the new
(negate-Y, negate-Z) matrix only on iPhone18,X — iPhone 17 Pro and
iPhone 17 Pro Max. Every other iPhone, including 15 Pro Max, falls
through to the original (swap-XY, negate-Z) matrix that was correct
pre-upstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PoissonRecon previously returned a NULL solver on degenerate input clouds
without propagation; SurfaceTrimmer's -1 return codes were discarded;
SCMeshTexturing then read a possibly-empty output PLY. Net effect: a sparse
cloud (e.g. from ICP diverging under head motion) produced an empty mesh or
hung the meshing progress UI with no surfacing error.

- MeshingOperation gains a failureReason property; main() stat-checks the
  Poisson and SurfaceTrimmer output PLYs and captures SurfaceTrimmerExecute's
  int return (already returned, just unused) to set a concrete reason.
- SCMeshingOperation re-exports failureReason from the C++ wrapper.
- SCMeshTexturing._meshPointCloud: refuses < 500-point inputs up front,
  checks operation.failureReason after run, stat-checks output existence,
  and rejects 0-vertex geometry rather than handing back an empty mesh.
- _buildAPIError now sets NSLocalizedDescriptionKey alongside the existing
  NSDebugDescriptionErrorKey so the failure message reaches
  error.localizedDescription in the SwiftUI consumer.

No upstream PoissonRecon source was modified -- detection is purely a thin
shim around output stat + return-code check, per plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an opt-in ARKit + Kabsch capture path alongside the legacy
AVCaptureSession one. Both pipelines live in the same build, gated by
UserDefaults; users opt in via kScanUseARFacePipelineDefaultsKey.

Phase B - ARFaceCameraManager + head-pose-prior accumulate overload.
- New ARFaceCameraManager (StandardCyborgUI) drives capture from an
  ARSession with ARFaceTrackingConfiguration. Per frame it converts the
  YUV captured image to BGRA, pulls capturedDepthData + calibration, and
  computes a camera-in-face delta from ARFaceAnchor.transform * inverse
  of the prior frame's same transform. Confidence collapses ARFaceAnchor
  .isTracked + ARCamera.trackingState into [0, 1].
- SCReconstructionManager gains a sibling -accumulate... method carrying
  the delta + confidence. _IncomingFrameData carries it through the input
  queue; the model-queue worker converts simd_float4x4 -> Eigen::Matrix4f
  and forwards to PBFModel.
- PBFModel::assimilate accepts an optional headPoseDelta + confidence.
  At confidence >= 0.9 it bypasses _runICP entirely and uses the prior
  as the frame transform -- this is what lets the pipeline fuse frames
  during head motion that plain ICP would reject.
- ScanningViewController now constructs CameraManager OR ARFaceCameraManager
  based on the UserDefaults flag, conforms to both delegate protocols, and
  routes both into a unified _handleFrame.

Phase C - reproject into head-anchor frame before fusion.
- Folded into Phase B's delta math: because we feed camera-in-face deltas,
  the accumulated _extrinsicMatrix naturally lives in face-frame, so fused
  surfels stay registered to the head as the head rotates in world. No
  separate C++ change required.

Phase D - Kabsch-based confidence validator.
- KabschRefiner (StandardCyborgUI, Swift+simd, no external deps) runs RANSAC
  + Kabsch rigid alignment on a subsampled face-mesh vertex constellation
  and returns an inlier fraction in [0, 1]. Rotation is recovered via
  polar decomposition (Higham iteration) to avoid pulling BLAS/LAPACK.
- ARFaceCameraManager wires it in behind kScanUseKabschRefinerDefaultsKey:
  when ARKit's per-frame delta disagrees with what its own face-mesh
  vertices say, confidence is linearly scaled down, which causes
  PBFModel to fall back to ICP for that frame.

Phase E hook (UI lives in the parent app) - facePoseObserver callback.
- ARFaceCameraManagerDelegate gains an optional arFaceCameraDidObserveFacePose
  method that fires every face-tracking frame with inv(camera) * face.
- ScanningViewController exposes a facePoseObserver closure forwarding
  the pose; the parent app's bridge installs the closure to drive the
  Face-ID-style overlay.

The OpticALLY parent app adds the overlay + UserDefaults wiring in a
separate commit on the parent repo (depends on this submodule bump).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants