Changelog¶
Unreleased¶
v4.16.10 — Lint & security patch (2026-05-27)¶
Fixed¶
- Lint: declare
VIBRATEpermission insceneviewlibrary manifest soHapticEngine'sVibrator.vibrate()calls no longer generateMissingPermissionlint errors in the library and its consumers. - Security: patch CVE-2026-8723 (medium) — pin
qstransitive dependency to>=6.15.2inmcp/packages/rerun,mcp/packages/interior,mcp/packages/gaming, andmcp-gatewayvia npmoverrides.
v4.16.9 — Sketchfab viewer polish + code quality (2026-05-27)¶
Fixed¶
- Feedback flow — the confirmation Snackbar after submitting a feedback report
now stays visible for the full
SnackbarDuration.Long(10 s) instead of the defaultShort(4 s), giving users enough time to read the "Feedback sent!" message before it disappears (#2230). - Sketchfab viewer — the loading sheet now shows a determinate
LinearProgressIndicator+X.X / Y.Y MBcounter while a GLB is streaming from Sketchfab, replacing the silent indeterminate spinner that gave no feedback during 20+ second downloads of heavy models. An advisory label ("Heavy model — may take a moment") appears for models ≥ 500k polys (#2232). - Sketchfab viewer — models no longer float on a blank background: a directional
light + invisible
plane_renderer_shadow.filamatplane at the model's ground level cast a soft contact shadow beneath every Sketchfab model (#2235).
v4.16.8 — Google Play 16 KB page-size + plane renderer polish (2026-05-27)¶
Fixed¶
- Fix Play Store upload rejection: enable 16 KB page-size alignment for native libraries in the demo AAB (
packaging.jniLibs.pageAlignSharedLibraries = true, required by Google Play since January 2026 for apps targeting Android 15+). - Plane renderer white-blob: tighter alpha cap (#2224 — second iteration). v4.16.4 dropped the alpha hard-cap from saturation to 0.45 but on-device QA in sunny outdoor scenes still read as an opaque white blob (45 % cool-white tint on already-light camera input ≈ 80 %+ perceived white). v4.16.5 tightens to a 0.20 alpha cap and caps
lineitself at 0.4 ingridLine()so the saturation is bounded at the source, not just clamped post-hoc. Grid coefficients also reduced (0.4 / 0.3 instead of 0.6 / 0.5). Industry baseline (Apple ARKit / Wayfair / ARCore Depth Lab reticle) ships plane viz at 20-30 % alpha unlit — this now matches. - Library 16 KB page-size alignment (#2226): add
experimentalProperties["android.nativeLibraryAlignmentPageSize"] = "16k"tosceneviewandarsceneviewlibrary modules so Filament's prebuilt.sofiles have ELF PT_LOAD segments aligned to 16 KB at pack time. Required for consumers' APKs to pass Google Play's new enforcement (Android 15+, enforced since January 2026). Consumers must also add this property to their own app-levelbuild.gradle.
v4.16.6 — 2026-05-27¶
Fixed¶
- macOS App Store: fixed all compile errors blocking macOS archive since #1049
(Xcode 16.2+). Guarded
navigationBarTitleDisplayMode,CADisplayLink,secondarySystemBackground, and AR demo scene destinations in#if os(iOS)blocks. Closes #1794.
v4.16.5 — 2026-05-27¶
Fixed¶
- Fix opaque white plane bug (#2224). At oblique camera angles (typical for AR floor planes) the V1 procedural grid shader's
fwidth(uv)saturated, collapsinggridLine()to ~1.0 across the whole plane and turning the detected ground into an opaque white blob. Three-lever fix: cool-white tint instead of pure white (MATERIAL_COLOR = Color(0.85, 0.90, 1.0)), grid alpha hard-capped at 0.45, denser cells viaBASE_UV_SCALE = 4.0(was 8.0) sofwidth(uv)stays in a stable range. Detected planes now read as a subtle translucent grid overlay, as intended by #1616. - iOS demo: fixed App Store archive crash — added
.gimbalcase to three exhaustive switch statements inCameraControlsDemothat were broken whenCameraControlMode.gimbalwas introduced in #1049 (Xcode 16.2+ treats missing enum cases as compile errors).
v4.16.3 — 2026-05-27¶
Fixed¶
- Fix iOS/macOS archive failure:
CameraControls.gimbalis only available in the iOS 18.2+ / macOS 15.2+ SDK (Xcode 16.2+). Guard it at compile time —.gimbalmode falls back to the orbit gesture path on SDKs older than 16.2. ExplicitRealityKit.CameraControls.*qualification added to all four native-mode cases to eliminate the type-inference ambiguity withSceneViewSwift.CameraControls.
v4.16.2 — 2026-05-27¶
Added¶
- iOS — native camera modes (
CameraControlMode): four new iOS-only cases (.none,.tilt,.dolly,.gimbal) delegate directly to Apple'srealityViewCameraControls(_:)modifier instead of SceneView's custom gesture math. The existing cross-platform modes (.orbit,.pan,.firstPerson) are unchanged — they keep orbit inertia, auto-rotate, and fit-to-bounds framing. Closes #1049 (Phase 2 — exposing the 4 Apple-only modes).
Changed¶
bridge-ios-compile.ymlis the first workflow opted into the self-hosted macOS runner introduced in #2192. Itsruns-onswitched frommacos-15to${{ vars.SELF_HOSTED_MACOS_ONLINE == 'true' && 'sceneview-mac' || 'macos-15' }}— when Thomas's Mac is online the type-check runs on bare metal (faster, nomacos-15minute spend), otherwise it falls back transparently to the GitHub-hosted runner. Picked as the pilot because of its low trigger frequency (path-gated onflutter/sceneview_flutter/ios/**+SceneViewSwift/**) — minimal blast radius if the self-hosted leg misbehaves. The PR itself touches the workflow file so the very push that lands this change validates the routing end-to-end.
Fixed¶
- Fix macOS archive failure:
CameraControls.gimbalis iOS-only — guard with#elseif os(macOS)and fall back to orbit gesture path on macOS (#2219 follow-up). - Fix Play Store upload rejection: enable 16 KB page-size alignment for native libraries in the demo AAB (
packaging.jniLibs.pageAlignSharedLibraries = true, required by Google Play since January 2026 for apps targeting Android 15+). - Bump MediaPipe Tasks Vision
0.20230731→0.10.26: pre-0.10.26 builds ship 4 KB-aligned ELF.sofiles that Google Play rejects with "Artifact does not support 16KB page size" (enforced January 2026). The same root cause was fixed in v4.15.4 on the release branch only — this backports the fix tomainso future releases are not affected. - Restored V1 as the default plane renderer (#2203). v4.16.0 briefly shipped V2 (depth-driven PBR mesh + HDR reflection + type-aware shading + scan-in) as the default, but on-device QA on a Pixel 9 showed the V2 visual output not matching the design intent — a washed-out translucent grid sheet instead of the promised HDR reflection + relief. V1 is restored as the default in v4.16.1 while V2 is polished. V2 stays available behind
ARSceneView(planeRendererVersion = PlaneRendererBase.Version.V2)as an experimental opt-in so early adopters can help shape the redesign. See.claude/plans/v2-references-study.md+v2-google-ar-catalog.md+v2-non-google-catalog.mdfor the comparative research (ARCore Depth Lab, Apple ARKit + RoomPlan, Niantic Lightship, Snap Lens Studio) that informs the next iteration.
v4.16.0 — 2026-05-26¶
Added¶
- Plane Renderer V2 — detected ARCore planes now render as a depth-driven PBR mesh
lit by ARCore's HDR estimate (#2203).
Floors, ceilings and walls each carry a distinct material identity, a brief scan-in
animation runs the first time a plane is detected, and the reflection ramps in over
~1 s to mask the HDR estimate stabilisation. The legacy flat-polygon renderer remains
available via
ARSceneView(planeRendererVersion = PlaneRendererBase.Version.V1)for one release cycle and is now@Deprecated. Includes a newar-plane-renderer-v2demo insamples/android-demowith a live V1 ↔ V2 toggle so the difference reads instantly. - Plane Renderer V2 — type-aware shading per
Plane.Type: a floor, a ceiling and a wall visible at once now read as three distinct surfaces. Floor (HORIZONTAL_UPWARD_FACING) renders cool-white withroughness 0.35; ceiling (HORIZONTAL_DOWNWARD_FACING) renders warm-white withroughness 0.65; wall (VERTICAL) renders neutral grey withroughness 0.80. Same singleMaterial, oneMaterialInstanceper plane — no extra Filament objects. ARCore re-classifications mid-tracking re-apply the preset on the next frame; unknown future plane types fall back to the floor preset rather than crashing. Opt in viaARSceneView(planeRendererVersion = PlaneRendererBase.Version.V2). PR #4 of #2203.
Changed¶
-
setup-self-hosted-runner.shv3 — install path moved from~/Library/Application Support/sceneview-runner/to~/sceneview-runner/. v2 picked the macOS-convention location which contains a space, breaking the runner's step-script invocation (/bin/bash -e <path>splits on the space →No such file or directory). The pilotbridge-ios-compilePR #2204 failed in 34 seconds on theSelect Xcodestep because of this exact issue (run id 26418464635). v3 keeps the LaunchAgent bootstrap design unchanged, only relocates the runner files. The installer auto-detects an existing v2 install at the legacy path, de-registers it from GitHub, and unloads its LaunchAgent before installing fresh — old files are left in place for manualrm -rf. -
Until the v3 runner is reinstalled, set the repo variable
SELF_HOSTED_MACOS_ONLINE=false(gh variable set SELF_HOSTED_MACOS_ONLINE -R sceneview/sceneview --body "false") so every opted-in workflow falls back tomacos-15. Re-running the v3 installer marks ittrueagain automatically via the heartbeat.
Fixed¶
- [Android AR] Fix
DepthMeshNodenever rendering its depth mesh —lastRebuildTimestampMswas initialised toLong.MIN_VALUE, causing the throttle guard (now - lastRebuildTimestampMs < refreshIntervalMs) to overflow to a large negative number on every frame and always return early. Changed to0Lso the first rebuild fires immediately as designed. (#2186) - [Android 3D] Fix
Nodetransform floating-point drift when updatingposition,quaternion, orscaleat high frame rates (60–120 Hz) — e.g.node.quaternion = newQin anonFrameloop (#2187). The root cause was that each individual-property setter decomposed the Filament 4×4 matrix to read the other two components, feeding float imprecision back on every tick. After ~10 000 frames the scale drifted visibly and the mesh warped. Fix: cache pristine TRS backing fields (_position,_quaternion,_scale) updated once on everytransformwrite; individual getters and setters use the caches, eliminating the matrix-decomposition round-trip. catmullRom(): Fix centripetal/chordal parameterisation — thealpha != 0path now uses the Barry-Goldman pyramidal recurrence over chord-length knots instead of the uniform matrix formula, soalpha = 0.5(centripetal) genuinely avoids cusps and self-intersections near sharp turns. The uniform path (alpha = 0) is unchanged.ModelLoader.createInstance(): Annotate with@MainThread— Filament'sAssetLoader.createInstance()is a JNI call that must run on the Filament main thread; the annotation surfaces a warning in the IDE and lint when called from a background coroutine.
v4.15.4 — 2026-05-26¶
Fixed¶
- Play Store deploy: set
inAppUpdatePriority: 3on every release upload (bothr0adkll/upload-google-playand the Python promote / fallback paths) so the in-appUpdateBanneractually fires when a new release lands. Pre-fix the workflow defaulted to priority 0 ("Google's discretion") and v4.15.2 silently never prompted v4.15.1 users —AppUpdateManager.appUpdateInforeturnedUPDATE_NOT_AVAILABLEfor days while Play Store itself indexed the release fine. Priority 3 = "high — surface within ~24h". Crash-fix releases can edit the workflow once to bump to 5 ("immediate"). (#2209) - Play Store production deploy unblocked. v4.15.3 production track 403'd on
:commitwithPERMISSION_DENIED — Artifact does not support 16KB page size. Root cause traced via 5 redispatches + 2 diagnostic PRs tolibmediapipe_tasks_vision_jni.sofrom MediaPipetasks-vision:0.10.14— its arm64 ELF was 4 KB-aligned (p_align = 0x1000). Filament 1.71.4 + ARCore 1.54.0 + Compose were all already 16 KB-aligned. Bumpedmediapipe-tasks-visionto0.10.26(the first release with "All the latest Android packages from Google Maven are now supporting the Android 16kb page size" per MediaPipe v0.10.26 release notes). Verified locally: rebuiltlibmediapipe_tasks_vision_jni.sonow reportsp_align = 0x4000. No API changes between 0.10.14 and 0.10.26 affect SceneView demo usage —compileReleaseKotlinclean. (#2214)
v4.15.3 — 2026-05-26¶
Changed¶
-
Self-hosted macOS runner infrastructure (opt-in) —
.claude/scripts/setup-self-hosted-runner.shinstallsactions/runner, writes a user LaunchAgent plist directly andlaunchctl bootstraps it (skippingactions/runner'ssvc.sh, which uses the deprecatedlaunchctl loadand fails on macOS 11+ withInput/output error; see actions/runner issue 1424), plus a second launchd heartbeat that updates the repo variablesSELF_HOSTED_MACOS_ONLINE/SELF_HOSTED_MACOS_LAST_SEEN. Workflows opt in by changingruns-on: macos-15toruns-on: ${{ vars.SELF_HOSTED_MACOS_ONLINE == 'true' && 'sceneview-mac' || 'macos-15' }}— the expression form supported by GitHub Actions since late-2024 — and fall back transparently to a GitHub-hosted runner when the Mac is asleep / off / the runner service is dead. The plist'sKeepAlive=truemakes the runner survive reboots, sleep/wake, and the runner's own auto-update cycle. Targets the 6macos-15jobs (ios.yml, bridge-ios-compile.yml, rn-ios-compile.yml, app-store.yml × 2, render-tests.yml) plus the NIGHTLY-ONLY iOS device-QA leg (#1601) that is currently skipped on per-push runs due to macOS-hosted cost. No existing workflow is modified by this commit. Inspired by Zach Rattner's M4 Mac cluster playbook. -
Biome v2 linter wired for
mcp/src/**/*.ts+mcp/scripts/**/*.js+website-static/js/sceneview.jsvia a repo-rootbiome.json. Usecd mcp && npm run biome(advisory) ornpm run biome:fix(auto-fix). Excludes generateddist/,mcp/src/generated/,__fixtures__/, vendoredqrcode-*.js, and Kotlin/JS-emittedsceneview-web.js. Not wired to CI for now — baseline reveals 216 errors / 236 warnings to clean up first. Adoption inspired by the same Mac-cluster playbook (Biome replaces ESLint/Prettier at Yembo). -
@claudemention bot —.github/workflows/claude.ymlruns the officialanthropics/claude-code-action@v1whenever a contributor drops@claudein an issue body/title, issue comment, PR review, or PR review comment. Auth viaCLAUDE_CODE_OAUTH_TOKEN(Claude Max subscription, no per-call API spend). Concurrency keyed per issue/PR so duelling replies are impossible. Setup is one-time:claude setup-token+gh secret set CLAUDE_CODE_OAUTH_TOKEN -b "<token>". Open-source contributors benefit too — they don't need an Anthropic account to ask Claude for help on a SceneView issue. -
SceneView statusline (
/.claude/scripts/statusline.sh, wired via.claude/settings.json) — shows branch,~worktree-slugmarker (so parallel sessions never confuse which checkout they're editing),VERSION_NAMEfromgradle.properties, free RAM in GB (useful for the emulator pool — flags when free RAM drops below the 3 GBEMU_MIN_FREE_RAM_MBfloor), and the active Claude model. No network calls; runs fast. -
CLAUDE.md trimmed 992 → 746 lines by deleting the nested "Previous state" session-state snapshots (lines 529-779 in the old file) — they were already mirrored chronologically in
.claude/handoff.md. CLAUDE.md now keeps only the current state + a stub pointing tohandoff.mdfor everything older. Every future session loads ~250 fewer lines of dead session log.
Fixed¶
- iOS App Store deploy: patch Swift 6 strict-concurrency error in
ARPlaneNodeDemo.swift:97that broke the v4.15.2app-store.ymlarchive step. Theprivate enum AssocKey { static var delegate = 0 }global (used only as anobjc_setAssociatedObjectkey) is nownonisolated(unsafe) static var delegate: UInt8 = 0— canonical opt-out for the "address-of-global as key" idiom. Fixes the v4.15.2 iOS deploy red without re-tagging.
v4.15.2 — iOS demo catalog parity sprint complete + Android crash burn-down (2026-05-26)¶
A double-headline release. iOS closes umbrella #910:
13 new SwiftUI demos (Augmented Faces, Depth Occlusion, Image Tracking, Plane Node, Point Cloud,
Collision, Debug Overlay, HDR Environment, Gesture Editing, People Occlusion, Body Tracker, Scene
Mesh, Reflection Probes, Shape Extrude, Texture Streaming, Video Texture) plus an append-only
demo-registry pattern (#1872) so future iOS demos can land as a single *Scene.swift file with
no project.pbxproj merge conflicts. Android ships a sweep of 5 user-visible regression
fixes (#2188,
#2191,
#2193,
#2194,
#2195) — the in-app feedback crash on
Play Store builds, an empty Sketchfab Explore tab (CloudFront WAF), a 5-second ANR on Sketchfab
preview, and chip-overlap UI papercuts. New cross-platform SceneMeshNode (#1760)
brings ARKit ARMeshAnchor parity to Android via ARCore StreetscapeGeometry. iOS gains three
native CameraControlMode cases (#1049
Phase 2) that delegate to Apple's realityViewCameraControls(_:) modifier. Install on Android via
Play Store internal track within minutes of tagging; iOS via TestFlight.
Added¶
- iOS — native camera modes (
CameraControlMode): three new native cases (.none,.tilt,.dolly) delegate directly to Apple'srealityViewCameraControls(_:)modifier (iOS 18+, macOS 15+, visionOS 2+) instead of SceneView's custom gesture math. The existing cross-platform modes (.orbit,.pan,.firstPerson) are unchanged — they keep orbit inertia, auto-rotate, and fit-to-bounds framing. Closes #1049 (Phase 2 — exposing the native Apple camera modes as verified in the Xcode SDK). - iOS deep-link registry widened to full demo catalog.
DemoDeepLinkRegistry.allowedIdsnow contains all 42 demo IDs (matching Android'sDemoRegistry.kt), so everysceneview://demo/<id>QR code is reachable on iOS — available demos open their real destination; coming-soon demos route to aDeepLinkPlaceholderinstead of silently dropping the link. Added missingdestination(for:)cases forAnimationDemo,ARInstantPlacementDemo,ARLightingDemo,ARRecorderDemo,MaterialsDemo,OrbitalARDemo,SceneGalleryDemoandMultiModelDemo(#1579). - iOS QA mode deep-link arg. Appending
?qa_mode=1to anysceneview://demo/<id>URL (or passing-qa_mode 1as a launch argument) writesUserDefaults["qa_mode"], which freezes auto-rotation inModelViewerScreenandSketchfabModelViewerScreenfor deterministic QA screenshots — mirrors Android'sqa_modeintent extra. Read from any view via@AppStorage(DeepLinkRouter.qaModeDefaultsKey)(#1579). - iOS QA:
lib/ios-axe.sh— helper script wrapping AXe (accessibility-driven iOS Simulator automation) for label-based taps, JSON UI-tree dumps, and screenshots. Mirrorslib/android-cli.sh's pattern; falls back gracefully toxcrun simctlwhen AXe is not installed. Implements slice 1 of the iOS device-QA parity plan. (#1673) SceneMeshNode— new ARCore node wrappingStreetscapeGeometrymeshes with unifiedMeshClassificationsemantics (#1760). Provides ARKitARMeshAnchorparity on Android: every face in the mesh is labelled with aMeshClassification(FLOOR, WALL, CEILING, TABLE, SEAT, WINDOW, DOOR, TERRAIN, BUILDING, UNLABELED) and anonClassifiedFace(faceIndex, classification)callback lets callers build per-face colour maps, physics layer masks, or audio zones. On ARCore the label is coarse (one classification per geometry — TERRAIN or BUILDING); on ARKit it is per-face (fine-grained indoor labels). The callback signature is identical on both platforms so the same consumer code compiles unchanged.ARSceneScope.SceneMeshNode(streetscapeGeometry, …)composable wired inARSceneScope; demo added asar-scene-meshin the Samples tab.- iOS demo: append-only demo registry pattern. Adding a new iOS demo now requires creating a single
*Scene.swiftfile with six header directives (@sceneId,@title,@subtitle,@icon,@category,@available); no other file needs editing.samples/ios-demo/scripts/collate-ios-demos.shdiscovers all scene files, sorts them by@sceneIdfor a stable diff, and emitsGeneratedScenes.swiftautomatically before each Xcode build via a "Collate iOS demos" Run Script phase.GeneratedScenes.swiftis.gitignored — parallel PRs adding different demos can never conflict on it (#1872). - iOS Augmented Faces demo (
ar-face): newARAugmentedFacesDemousingARFaceTrackingConfiguration+AnchorEntity(.face); ring of coloured spheres orbiting the face pose tracked by TrueDepth camera (iPhone X+); simulator placeholder for non-device builds. Promotesar-facefrom deep-link placeholder to a full iOS demo. - iOS AR Depth Occlusion demo (
ar-depth-occlusion): newARDepthOcclusionDemousingSceneReconstructionNode.enableOcclusion()for LiDAR-powered real-world depth masking; toggle to enable/disable occlusion at runtime; graceful fallback banner for non-LiDAR devices; simulator placeholder. Promotesar-depth-occlusionfrom deep-link placeholder to a full iOS demo. - iOS AR Image Tracking demo (
ar-image): newARImageTrackingDemousingAugmentedImageNode.createImageDatabase()with a bundled QR code reference image; 3D cube overlaid on detected image; simulator placeholder shown on non-device builds. Promotesar-imagefrom deep-link placeholder to a full iOS demo. - iOS AR Plane Node demo (
ar-plane-node): detects ARKit horizontal and vertical planes, places a translucent blue marker cube at each plane centre, and displays a live plane-count pill. Mirrors AndroidARPlaneNodeDemo. (#910) - iOS AR Point Cloud demo (
ar-point-cloud): renders ARKit live tracking feature points viaARView.debugOptions.showFeaturePoints, shows a live point-count pill, and offers a toggle to enable/disable the overlay. Mirrors AndroidARPointCloudDemo. (#910) - iOS — Collision & Hit Test demo: port the
collisiondemo from placeholder to a full implementation — fiveGeometryNodeshapes (cubes and spheres) are tap-highlighted viaSceneView.onEntityTapped; an on-screen "Reset Colors" button clears all highlights; Maestrointeraction.yamlpromoted fromplaceholder.yamlsmoke to a realdemo.yamlflow (#910). - iOS demo: Debug Overlay — RealityKit sphere stress test with live FPS stats, frame time, node/triangle counts, and a rolling FPS sparkline. Matches Android's
DebugOverlayDemo: preset buttons (1/10/100/500/1 000 spheres), progressive spawn, and a 10-second stress ramp from 1 → 1 000 spheres.sceneview://demo/debug-overlaynow routes to the real demo instead of the coming-soon placeholder. (#910) - iOS — HDR Environment demo: port the
environmentdemo from placeholder to a full SwiftUI implementation —SceneViewDemonow shows a.demoSettingsSheetwith a grid of environment presets (.studio,.outdoor,.sunset,.night,.warm,.autumn,.nightSky) switchable at runtime; Maestrolighting.yamlpromoted from placeholder todemo-settings.yamlsmoke (#910). - iOS — Gesture Editing demo: port the
gesture-editingdemo from placeholder to a full implementation — aModelNode(ferrari_f40) is draggable, pinch-scalable, and two-finger-rotatable in Edit Mode; camera orbits freely in View Mode; settings sheet shows a mode toggle, Reset button, and live transform readout (#910). - iOS demo — Occlusion Material: new
OcclusionMaterialDemoshows RealityKit's built-inOcclusionMaterialin action — an invisible, depth-writing plane that cuts a sphere, with a toggle to reveal the occluder as a semi-transparent slab. Reachable viasceneview://demo/occlusion-material. Closes the last pure-3D gap in the iOS Advanced category relative to the Android catalog (#910). - iOS AR People Occlusion demo (
ar-people-occlusion): toggle ARKitpersonSegmentationWithDepthto hide virtual cubes behind real people walking in front; requires A12+ chip (#910). - iOS AR Body Tracker demo (
ar-body-tracker):ARBodyTrackingConfiguration+ RealityKitBodyTrackedEntitymarks the detected skeleton root joint in real time; requires A12+ chip (#910). - iOS AR Scene Mesh demo (
ar-scene-mesh):ARWorldTrackingConfiguration.sceneReconstruction = .meshWithClassificationbuilds a live LiDAR mesh with a debug wireframe toggle; requires LiDAR device (#910). - iOS — Reflection Probes demo: port the
reflection-probesdemo from a placeholder to a full SwiftUI implementation usingReflectionProbeNode. Shows a metallic sphere and cubes with varying metallic values inside a box probe zone; an environment picker switches between four IBL presets (Sunset, Night Sky, Studio, Outdoor) with a live intensity slider. - iOS — Shape Extrude demo: port the
shapedemo from a placeholder to a full SwiftUI implementation usingShapeNode. Six preset shapes (Triangle, Star, Pentagon, Hexagon, L-Shape, Arrow) with adjustable extrusion depth slider (0–0.4 m) and a PBR/unlit material toggle. - iOS demo: add Texture Streaming demo (
sceneview://demo/texture-streaming) — interactive PBR material preset switcher (Gold/Silver/Copper/Ceramic/Plastic/Rubber) on a sphere usingPhysicallyBasedMaterial; teaches runtime material swap without geometry rebuild (#910).
Changed¶
- Bump Filament from 1.71.0 to 1.71.4 (patch — no
.filamatrecompile needed; includes Metal async resource loading, bounds-check fixes infilaflat, and iOS arm64 simulator support in Xcode 16+) (#2156). - Refresh store listing assets: update app icon (Android + iOS) to the canonical 3D isometric cube branding, regenerate feature graphic ("3D and AR for Android, iOS & Web"), and replace all App Store / Play Store screenshots with fresh captures (#2180).
- Bump Compose BOM to
2026.05.01(commit6a2b4b4d1).
Fixed¶
- Xcode project registration for new AR demos.
ARPeopleOcclusionDemo,ARBodyTrackerDemo,ARSceneMeshDemo, and their scene-registry files were not registered in the Xcode project's Sources build phase — fixed alongside the new demos so the iOS targets actually compile them. (#910) - CI (
app-store.ymlsubmit step): switch from the legacyappStoreVersionSubmissionsAPI to App Store Connect'sreviewSubmissionsAPI v3 (2023+). The old endpoint returned403 "Allowed operation is: DELETE"whenever a stale submission was attached to an absorbed draft (the #1687 / #1795 retargeting pattern); the read permission needed to find that stale submission was not in scope on our deploy service account. The new flow (POST /v1/reviewSubmissions+POST /v1/reviewSubmissionItems+PATCH submitted: true) is independent of any legacy submission state, so the 403 class is eliminated entirely. Closes the long-running #1831 saga end-to-end (#2141 closes #1831). - ARFaceDemo: front-camera unavailability diagnostic. Added a 5-second timeout after which, if no AR frame has been received, the status pill turns red and reads "Front camera unavailable on this device". This surfaces the silent black-screen regression on Pixel 9 (#1612) where
frontCameraConfigmay fall back to the BACK camera, leaving the selfie feed dead without any user-visible error. - CI (quality-gate):
feedback-workernpm testis now run as part of the quality gate — a future regression in the worker is caught on every PR that touchesfeedback-worker/. (#2032) - Feedback (Android demo): lower the screen-recording size cap from 28 MB to 25 MB to give 5 MB of headroom for the AAC audio track + multipart envelope (vs the previous ~2 MB) before the worker's 30 MB 413 threshold. (#2032)
- Feedback (FeedbackContextTest): fix stale KDoc mentioning the removed
routekey; addisEmulator()reachability test. (#2032) - iOS (SceneViewSwift):
SceneEntities.deinitno longer traps if the instance is released off the main thread. ReplacedMainActor.assumeIsolatedwith an explicitThread.isMainThreadguard +DispatchQueue.main.syncfallback so an off-main release degrades gracefully instead of crashing. (#2068) - iOS demo (samples):
ModelViewerDemo,PhysicsDemo, andSpatialAudioDemonow set.environment(.studio)on theirSceneView— matching the android-demo IBL fix (#2110) so metallic glTF models are consistently lit across the iOS demo catalog. (#2114) - CI: CI Gate no longer hard-fails on docs-only PRs. A 90-second grace period replaces the previous 50-minute timeout — if no other check runs register (because every workflow was path-filtered out), the gate exits green immediately. (#2117)
- Fix Play Store CI deploys blocked by undeclared Foreground Service (FGS) permission (#2120).
The production fallback now preserves the staged edit in Play Console (instead of deleting it
on FGS failure), making the FGS declaration section visible under App content. A new
commit_edit_idfast-path inworkflow_dispatchlets you commit the preserved edit in ~2 min after declaring FGS — no 40-min rebuild needed. - ARSceneView:
detectConfigDowngradesnow captures the post-sessionConfiguration-callback depth mode, so a callback-driven depth-mode request that gets silently downgraded is correctly surfaced asARConfigDowngrade.DepthMode. (#2122 / #2096 gap 1) - MaterialsDemo: fixed infinite "Loading…" scrim when the
materialsregistry category is empty (null selected slug now exits to anEmptystate instead of staying inLoadingforever). (#2122) - Feedback (Android demo): detect emulator in
FeedbackContext(isEmulatorflag). The review screen now shows a warning hint when submitting from an emulator without a typed note, since emulator mics are silent and Whisper returns an empty transcript. (#2123) - Feedback (worker): the GitHub issue body now explains why there is no transcript when both transcript and typed text are empty: emulator submissions get a specific "no physical mic" message; other silent-audio cases get a generic explanation. A maintainer note is added to avoid confusion when an issue has no actionable content. (#2123)
samples/android-demo/build.gradle: honour-PversionNamefrom Play Store workflow — versionName was hardcoded, causing Play Console to show the stale name from the build.gradle source instead of the release tag.- Fix
.well-known/assetlinks.jsonandapple-app-site-associationreturning HTTP 404 onsceneview.github.io—upload-artifact@v7silently stripped dot-prefixed directories unlessinclude-hidden-files: trueis set, causing the deploy job's patch step to fail (#2155). docs.yml: fix/.well-known/files returning HTTP 404 onsceneview.github.io—peaceiris/actions-gh-pages's internalshelljs cpglob does not expand dot-prefixed subdirectories, soassetlinks.jsonandapple-app-site-associationwere silently dropped on every deploy; a post-deploy patch step now adds the missing directory via a direct SSH git commit (#2155).- iOS registry: remove stale
ar-eis/ar-pose-placementdeep-link aliases — the canonical Android IDs (ar-image-stabilization,ar-pose) were already present inallowedIds; the aliases were unreachable duplicates that silently droppedsceneview://demo/ar-image-stabilizationQR-code taps. (#2173) - iOS demo — renamed placeholder scenes
ArEisScene→ArImageStabilizationSceneandArPosePlacementScene→ArPoseSceneso their@sceneIddirectives match the canonical Android IDs (ar-image-stabilization,ar-pose) used by QR codes and deep links; closes the gap left by #2174 which fixedallowedIdsbut not the scene catalogue. - Fix
device-qa.shcrash on macOS (timeout: command not found):lib/maestro.shnow falls back togtimeout(homebrew coreutils) or runs unbounded when neither GNUtimeoutvariant is available (#2184). - Feedback (Android demo): stop crashing the app when the user triggers screen recording on a Play Store build that ships without
FOREGROUND_SERVICE_MEDIA_PROJECTION(the #2120 catch-22).FeedbackRecordingService.isRecordingAvailable()now detects the missing typed-FGS permission on Android 14+; the flow short-circuits to text + audio beforestartForegroundServiceraisesForegroundServiceDidNotStartInTimeException.start()/stop()are also belt-and-suspenders try/caught. Robolectric regression suite locks the SDK-gated behaviour. (#2188) - Sketchfab (Android demo Explore tab): repair the silently-empty Discover/Gallery/Tutorials carousels. AWS CloudFront's WAF in front of
api.sketchfab.comwas returning HTTP 202 + an empty body +x-amzn-waf-action: challengeto any request carrying OkHttp's defaultUser-Agent: okhttp/<version>(treated as bot traffic), so the JSON decoder threwExpected start of the object '{', but had 'EOF' instead, each feed swallowed the error, and the user saw a half-rendered Explore tab. Now sends an explicit app-identifyingSceneViewDemo/<version> (Android; +https://sceneview.github.io)User-Agent and surfaces a typedWafChallengeerror so the "Sketchfab unavailable" banner explains the state instead of three self-hiding carousels. (#2191) - Sketchfab (Android demo): stop the 5+ second ANR when opening the model preview sheet. The Filament
Engineis now pre-warmed at the sheet root on the first transition out ofPreview(gated bystage !is Preview), so the ~5 s synchronous JNI cost overlaps with the Ken-Burns + spinner UI of theDownloadingstage instead of (a) blocking the user's card-tap on a stale Explore-tab background (the original ANR) or (b) freezing on a stopped-spinner moment betweenDownloadingcompletion andRendering(an earlier partial fix). The Engine slot survives the Downloading → Rendering transition, so the model appears the instantrememberModelInstancefinishes parsing the GLB — no second freeze. (#2193) - Feedback chip (Android demo): stop masking the bottom row of content across tabs. Introduces a shared
FEEDBACK_FAB_RESERVED_SPACEconstant (in the newfeedback/FeedbackChrome.kt) applied as bottomcontentPaddingon the Samples grid, the About column, and the AR-View launcher column, so the floating chip floats over a gutter rather than over the last items. The chip is also hidden while the liveARSceneViewis on screen (via aDisposableEffecttogglingFeedbackChrome.chipVisible), so the AR-View bottom action bar (model picker + Reset + Share) is no longer half-masked on the left. (#2194) - AR-View "Try an AR demo" tiles (Android demo): stop rendering the same generic
Icons.Filled.ViewInAron every tile.FeaturedArDemonow carries a per-demoImageVector(AddLocationAlt,Face,Cloud,LocationCity,Layers,SelfImprovement) so users can tell the 6 demos apart at a glance — matching the Samples-tab grid where each demo already had a unique icon. (#2195) - iOS — Video Texture demo: add
VideoTextureDemo.swiftandVideos/sample.mp4to the Xcode project (project.pbxproj) so thevideodemo that was already implemented (but orphaned) now compiles and runs. FixesGeometryNode.plane(width:height:)call to use the correctwidth:depth:parameter. - Fixed
BillboardNodesilently ignoring billboard rotation on macOS. The#available(iOS 18.0, visionOS 2.0, *)guard inBillboardNode.init(child:)excluded macOS, soBillboardComponentwas never applied and entities faced a fixed direction instead of the camera. SinceSceneViewSwiftrequires macOS 15+ (which shipsBillboardComponent), the guard is removed. Added a Platform Support table toSceneViewSwift/README.mddocumenting thatSceneView(3D) is fully supported on macOS butARSceneViewis iOS-only (#914). - iOS demo (SketchfabService):
downloadBinarynow surfaces real download progress instead of always emitting1.0at completion. ReplacedURLSession.download(from:)(no intermediate callbacks) with aURLSessionDownloadDelegatethat reports per-byte progress, so the model viewer's progress bar animates smoothly on slow connections. (#982) - Fixed iOS AR screenshot capturing a black hole instead of 3D content.
ARTab.shareARScreenshotpreviously usedUIView.drawHierarchy, which skips the Metal layer and produces a transparent / black hole where the 3D AR content lives. Now usesARView.snapshot(saveToHDR:completion:)— RealityKit's Metal-aware capture path — which correctly captures both the camera background and 3D content. The simulator path shows a user-friendly "AR screenshots require a physical device" message instead of producing a broken image (#983). sceneview-webREADME CDN/API mismatch fixed. The README marketed a non-existentsceneview.jsCDN file and aSceneView.modelViewer(...)global with methods (setQuality,setBloom,addLight,createText/Image/Video, …) the build never exposed — every<script>snippet 404'd and the API table was fiction. It now documents the realsceneview-web.jsartifact path and the actualwindow.sceneviewAPI surface (createViewer,modelViewer, and theSceneViewerinstance methods), matchingsceneview-web.d.ts.sceneview-webnow ships its TypeScript declarations.package.jsongained a"types": "sceneview-web.d.ts"field and the hand-written.d.tsis now infiles[], so TS consumers get typings instead ofany.sceneview-mcpsceneview://known-issuesresource no longer crashes on malformed GitHub API items. The issue type guard validated onlynumber/title, thenformatIssuesunconditionally readissue.user.login,issue.labelsandissue.updated_at— a partial API item (e.g. during a GitHub incident) threw aTypeErrorand took down the whole resource. Items are now normalized with safe defaults foruser,labelsandupdated_at.
Tests¶
- iOS deep-link registry: sync
DemoDeepLinkRegistry.allowedIdsto the full Android catalog (65 IDs covering all 60 Android demo IDs) — 23 new AR and 3D demo IDs added so QR codes for newer demos no longer silently 404 on iOS; corresponding placeholder flows added to Maestro.maestro/ios/for CI smoke coverage. - Android Maestro: expanded demo coverage from 43 to 58 demos — added 13 missing AR demos (
ar-depth-of-field,ar-fog,ar-depth-collider,ar-depth-visualization,ar-people-occlusion,ar-point-cloud,ar-raw-depth-point-cloud,ar-plane-node,ar-scene-mesh,ar-scene-semantics,ar-ml-object-label,placement-scene,ar-collaborative,ar-body-tracker) and 2 Advanced demos (occlusion-material,spatial-audio) to the device-QA harness (#1913).
Docs¶
- iOS — Scene Reconstruction parity: update
cheatsheet-ios.mdandllms.txtto markSceneReconstructionNode(renderable mesh) andenablePhysics(in:)(physics collider) as Available — closes the documentation gap from #1860. The library wrapper ships since the earlierSceneReconstructionNode.swiftimplementation. - docs(ios) —
samples-ios.mdrefreshed with the full 59-demo iOS catalog table (3D Basics, Lighting, Content, Interaction, Advanced, AR) and updated minimal working examples including the newCameraControlModenative Apple modes (.none,.tilt,.dolly, iOS 18+). Closes the documentation gap left after the iOS parity sprint (umbrella #910).
v4.15.1 — Play Store R8 deploy fix + burn-down sweep: black-model IBL, Sketchfab repair, demo-hang & macOS-archive fixes (2026-05-22)¶
Added¶
- Surface AR camera-config / depth-mode downgrades (#2096).
ARSceneViewnow exposes anonConfigDowngradedcallback that fires with a typedARConfigDowngrade(DepthModeorCameraConfig) when a requested capability is unsupported on the device and is silently downgraded to a working fallback — so apps can adapt their UI instead of behaviour diverging silently across devices.
Fixed¶
- macOS demo target archives again (#1794). Guarded iOS-only SwiftUI APIs in the shared
samples/ios-demoSwift source so theSceneViewDemomacOS target compiles: added cross-platformColor.systemBackground/secondarySystemBackground/tertiarySystemBackgroundhelpers and anavigationBarTitleInline()modifier inTheme.swift,#if os(iOS)-guarded the iOS 18.zoom(sourceID:in:)navigation transition and thetopBarTrailingtoolbar placement. - Demo settings sheet remembers its last detent per demo (#2084). The demo-app settings bottom sheet now reopens at the detent (partially-expanded vs fully-expanded) the user last left it at, individually for each demo. The detent is persisted in
SharedPreferenceskeyed by demo title, so it survives navigating away from the demo and full process death. A demo never opened before still defaults to the partial detent. materialsandscene-galleryAndroid demos no longer hang on "Streaming…" (#2088). A failed model resolution is now captured into an error state and surfaced with an error scrim and a Retry button, instead of being swallowed into anullpath that left the loading scrim spinning forever. Both demos are flaggedDemoStatus.KnownIssueso the Samples grid shows an honest known-issue chip.- Sketchfab integration repaired in the demo apps (#2095). All 29 Stage-1 placeholder model uids in
SampleAssets.kt/SampleAssets.swiftwere fabricated and returned HTTP 404, silently breaking every streamed sample demo. They are replaced with 29 real, API-validated Sketchfab models (each verified200+isDownloadable: true+ CC-BY 4.0). Thematerialsandscene-gallerydemos no longer hang and are restored fromKnownIssuetoWorking(reverting the #2088 stopgap). The Android Explore feed now shows the "Sketchfab unavailable" banner when the API key is rejected with HTTP 401/403 instead of silently collapsing to an empty feed, an OkHttp disk cache was added toSketchfabServiceto cut rate-limit (429) pressure on basic-plan keys, and theverify-sketchfab-keyCI step now runs onworkflow_dispatchrelease paths, not only tag pushes. - Fixed the
android-demorelease AAB build failing atminifyReleaseWithR8withMissing class javax.lang.model.**. MediaPipe'stasks-visionPOM dragged the fullcom.google.auto.value:auto-valueannotation processor (and a shaded JavaPoet) onto the runtime/minify classpath; R8 full-mode promoted the compile-time-onlyjavax.lang.model.**JDK classes to a hard error. AutoValue is now excluded from thetasks-visiondependency and matching-dontwarnkeep rules were added, unblocking the Play Store release. (#2106) - glTF models no longer render solid black in demos that didn't set an environment (#2110). A glTF model with metallic / smooth PBR materials needs an image-based-lighting (IBL) environment to reflect. SceneView's default environment is a lightweight neutral IBL paired with a solid black skybox, so metallic surfaces had nothing bright to reflect and rendered black. The non-AR model demos (
FogDemo,CameraControlsDemo,GestureEditingDemo,PostProcessingDemo,OcclusionMaterialDemo,SceneGalleryDemo,ModelViewerDemo) now share arememberModelDemoEnvironmenthelper that supplies the bundled studio HDR IBL (the same one the multi-model scene uses), so the Damaged Helmet and other PBR models are correctly lit. No model materials were overridden. - Fresh iOS App Store screenshots +
-demolaunch argument (#917). The App Store Connect listing carried stale screenshots — Android-device captures, several of them blank white AR scenes, and phone images letterboxed onto the iPad canvas. A new set of genuine iOS-simulator captures showing real rendered 3D content now ships undersamples/ios-demo/appstore-screenshots/(iphone-6.9/at 1320×2868,ipad-13/at 2064×2752), regenerable via.claude/scripts/capture-appstore-screenshots.sh. The demo app gained a-demo <id>launch argument that routes straight to a demo on first frame (reusingDemoDeepLinkRegistry), giving the capture pipeline a deterministic, dialog-free entry point alongside the existingsceneview://demo/<id>user-facing deep link.
Tests¶
- Deterministic
CollaborativeSessionTest(#2091). The'hello propagates a participant'test (and its siblings) intermittently failed on CI with a coroutineTimeoutCancellationException: cross-session message propagation ran onDispatchers.Defaultwhile assertions polled a realwithTimeout, so a contended runner could starve the thread pool past the deadline.CollaborativeSessionnow accepts an injectable I/O dispatcher (test-only, production unchanged) and the test drives propagation on aStandardTestDispatcherwithrunTestvirtual time — no thread pool, no wall-clock race.
v4.15.0 — Cross-platform bridge & audit hardening: Flutter/RN iOS bridges, resource-leak sweep, CI-drift fixes (2026-05-22)¶
Added¶
- "Replay + analyse" mode in the AR Recording demo (#2027). The android-demo AR Recording demo gains a fourth mode that replays a dataset and interprets it: every replayed frame is folded through the
ARRecordInterpreterlibrary API and the runningARRecordInterpretation— tracked-frame %, trajectory length, dominantTrackingFailureReason, plane count/area — is overlaid live on the replay. When the dataset ends (rememberARPlaybackStatus == FINISHED) a final report card sums up the take with a green/amber tracking verdict and a per-failure-reason breakdown.
Changed¶
- AR demos: fitting per-demo content instead of the generic Damaged Helmet (#2023). Nine Android AR demos no longer load
khronos_damaged_helmet.glbpurely as a generic floating stand-in. The six already-bundled models are redistributed so each demo's placed object reads as intentional content grounded in the room: Image Stabilization and Cloud Anchor use the lantern, Depth of Field and Augmented Image use the toy car, Terrain Anchor and Rerun use the fox / Shiba, Record & Playback uses the fox, and both placement cycles (ARPlacementDemo,ARInstantPlacementDemo) swap the helmet entry for the upright Soldier character. The helmet is kept only in the two occlusion demos (Depth Occlusion, People Occlusion), where a hard-surface PBR payload genuinely fits.
Fixed¶
- feedback-worker: minor cleanups deferred from the hardening review (#2028). The
202response now carries areasonfield ("quota"vs"github_error") so a caller can tell a deliberate issue-quota throttle from a GitHub-side failure. An empty or whitespace-onlyContent-Lengthheader is now rejected with411instead of slipping through as a zero-length body (Number("")is0). The Whisper-detected transcript language is surfaced as aTranscript languagerow in the GitHub issue context table instead of being discarded. The unused'purged'value was dropped from thefeedback.statusCHECK constraint (media expiry is tracked bymedia_purged, neverstatus). - android-demo: in-app feedback — lower-priority review follow-ups (#2030). Rotating the device mid-recording no longer leaves the rest of the clip stretched —
FeedbackRecordingServicenow re-fits theVirtualDisplayto the rotated screen aspect inside the fixed encoder surface (deliberate, centred letterboxing) on a configuration change. AMediaProjectionrevoked by the system or another app is now reported with a distinct "Recording stopped early" message instead of the generic "recording didn't work" copy. The tab-screen feedback FAB collapses to an accessible icon-only FAB on a narrow screen or at a large font scale, so the extended label can no longer overflow or crowd the navigation bar. The "My feedback" screen gains a "Refresh status" action that drops the 5-minute GitHub status cache so a user can re-check a ticket immediately. - Fixed five resource leaks in the Android core libraries.
Node.destroy()now recursively destroys its children as documented (#2036);MeshNodecan free its ownedVertexBuffer/IndexBufferandStreetscapeGeometryNodeopts in (#2037);ARCameraStream.destroy()releases itsIndexBuffer(#2039);Delaunator'slegalize()stack grows on demand instead of silently dropping edges (#2041); and theAnchorNode.anchorsetter detaches the replaced ARCore anchor (#2043). - iOS
NodeGestureno longer leaks entities (#2038). Per-entity gesture handlers are now stored in a RealityKit component attached to the target entity instead of in process-globalstaticdictionaries. The handler closures live exactly as long as the entity does — the commononDrag(cube.entity) { cube.position += … }capture pattern no longer leaks the entity and its resources for the whole process lifetime — and twoSceneViewinstances can no longer share or wipe each other's gesture state.removeAllHandlers()is replaced by the scene-scopedremoveAllHandlers(under:). - iOS
CameraControlsconvenience initminRadiusdefault corrected to1.0(#2040). It previously defaulted to the pre-v4.4.0 value0.5, which clips the perspective camera into geometry on the true-camera orbit path — soCameraControls(mode:sensitivity:)silently re-introduced the bug. Both initializers now agree. - iOS
ViewNode<Content>documentation is now honest (#2042).ViewNodecurrently renders a placeholder white plane and does not display the SwiftUIcontentit is given (the UIView→texture pipeline is tracked by #1035). The type is now marked@available(*, deprecated)and its doc-comment no longer claims interactive SwiftUI rendering. - iOS removed 17 dead
#if os(...)guards (#2044). Inner#if os(iOS) || os(visionOS) || os(macOS)guards nested inside identical always-true file-level guards across 12SceneViewSwiftfiles were removed (the code inside stays). A note inCONTRIBUTING.mdkeeps new code from re-introducing them. - Web XR sessions no longer leak the Filament engine + WebGL context (#2045).
WebXRSession,ARSceneViewandVRSceneViewnow destroy theSceneViewthey created when the session ends — both viastop()and via theonendhandler (system-UI / headset-menu exit) — with an idempotency guard so the two paths cannot double-free. - Web XR render loop applies the per-eye projection matrix and renders both eyes (#2046). Each frame now sets the Filament camera from
XRView.projectionMatrix(correct FOV / passthrough registration) via a newCamera.setCustomProjectionbinding, and the VR path renders everyXRViewinto its own viewport instead of onlyviews[0]— VR is now genuinely stereo. - Web:
createViewerImplno longer leaks awindowresize listener (#2048). The untracked, never-removed listener was redundant withSceneView.autoResize(which also updates the viewport + projection) — it has been removed. - Web:
sceneview-web.d.tsmatches the Kotlin source (#2057).setAutoRotateSpeedis documented as radians per frame (was wrongly "per second" — a ~60x speed error for consumers), the missingsetAutoCenterContentmethod is now declared, and the stale version example is refreshed. - Web: committed
sceneview-web/package.jsonno longer carries misleading publish fields (#2058). Themain/files/publishConfigentries pointed at abuild/dist/js/...path the build never produces; they are removed (CI'srelease.ymlgenerates the real published manifest) with a comment recording thatrelease.ymlis the single source of truth. mcp/dist/is no longer committed to git (#2047). The compiledtscoutput was tracked in version control yet regenerated by thepreparescript on everynpm install/npm publish, so it silently drifted fromsrc/— the committeddist/generated/llms-txt.jsanddist/generated/version.jsembedded a SceneView SDK version three minor releases stale.mcp/dist/is now fully.gitignored (the npm tarball is always built fresh on publish), and the.gitignoretest-artefact glob is widened frommcp/dist/*.test.jsto the whole directory so nested compiled test files are never tracked.- Flutter:
SceneView/ARSceneViewno longer dispose a caller-owned controller (#2050). The widget now disposes only the controller it created itself; a controller passed in by the caller is left untouched, so controller reuse and widget re-parenting no longer break. - Flutter iOS: 3D
onTapcallback now fires (#2051). The iOS bridge wires SceneViewSwift's entity hit-test to theonTapmethod channel, matching Android. ARonTap/onPlaneDetectedremain Android-only for now — the Dart docs now state this explicitly instead of implying parity. - Flutter iOS: platform views no longer leak the RealityKit scene (#2052). Both iOS platform-view classes now detach the
FlutterMethodChannelhandler indeinit, breaking the retain cycle that kept the hosting controller, ARSession, and scene alive after the Flutter widget was disposed. - React Native:
onTap/onPlaneDetectedevents now actually fire (#2053). The two event props were exported but never dispatched. Android now registers them viagetExportedCustomDirectEventTypeConstantsand dispatches aTapEvent(tapped node name + world position) /PlaneDetectedEvent(one per newly-tracked ARCore plane) through the view'sEventDispatcher. iOS wiresonTapto SceneViewSwift's tap callback. - React Native iOS:
geometryNodes/lightNodesparity gap disclosed (#2054). The props are fully rendered on Android but not on the iOS RealityKit bridge — the TypeScript doc comments now state the iOS limitation explicitly instead of silently dropping the props. - React Native:
ARSceneViewdepthOcclusion/instantPlacementwired to the AR session (#2055). Android now forwards both flags to ARCore viaConfig.DepthMode.AUTOMATIC/Config.InstantPlacementMode.LOCAL_Y_UP; the iOS gap (no SceneViewSwift knob) is disclosed in the TypeScript doc comments. - React Native: podspec git tag fixed (#2056).
react-native-sceneview.podspecresolved its git source to the bare version (4.14.0) but the repo's release tags arev-prefixed; the source tag is nowv#{s.version}. - Flutter plugin's iOS bridge now compiles against the real SceneViewSwift API (#2065).
SceneViewSwiftUIWrapper/ARSceneViewSwiftUIWrapperreferenced APIs that do not exist onSceneViewSwift—ModelNode(path)(noStringinitialiser) andForEachinside@NodeBuilder(unsupported) — so the iOS plugin never compiled. The wrappers now use the real imperativeSceneView { (Entity) -> Void }content closure and the asyncModelNode.load(_:)API, streaming models in via a persistent content root (3D) and tap-to-place anchoring (AR). A pre-existing Swift 6 actor-isolation error (SceneState()constructed off the main actor) is also fixed. A newbridge-ios-compile.ymlworkflow type-checks the plugin's iOS Swift against the published SceneViewSwift module on every PR so this can't regress. - React Native iOS bridge now compiles against the real
SceneViewSwiftAPI (#2067).RNSceneViewContent/RNARSceneViewContentreferenced APIs that do not exist (ModelNode(String), anARSceneView { anchor in … }content closure, anoverridenrequiresMainQueueSetupon a plainNSObject), so the module's iOS support never built. The bridge is rewritten to use the genuineSceneViewSwiftsurface — asyncModelNode.load(_:),SceneView's imperative content init, andARSceneView'sonSessionStarted/onTapOnPlane— and a newrn-ios-compile.ymlCI workflow type-checksreact-native/react-native-sceneview/ios/*.swiftagainst the real package so this can't regress. The podspec no longer declares a CocoaPodss.dependencyonSceneViewSwift(it is SwiftPM-only); the README documents adding it via Xcode's Swift Package Manager. Companion fix to #2065 (Flutter). - flutter ios: actually break the method-channel retain cycle (#2069). the platform-view's
setMethodCallHandlernow installs the handler with a[weak self]capture; a bare method reference strong-heldself, so the previously addeddeinitcould never run and the platform view, hosting controller, and RealityKit/AR scene still leaked on every create/dispose cycle. - React Native Android:
ARSceneViewdepthOcclusion/instantPlacementnow apply on a live AR session (#2070). PR #2066 forwarded both flags through the consumedarsceneview:4.7.0sessionConfigurationcallback, but that callback runs only once at session creation — toggling either prop from JS afterwards was a silent no-op. The RN manager now captures the live ARCoreSessionviaonSessionCreatedand re-applies theConfigfrom aLaunchedEffectkeyed on the two flags, so a runtimedepthOcclusion/instantPlacementtoggle genuinely reconfigures the running session. - CI: two recurring drift classes made structurally impossible (#2071). The
docs/docs/llms.txtmirror of rootllms.txtis no longer committed — it is regenerated from rootllms.txtat docs-build time (docs.yml, beforemkdocs build) and.gitignored, so it can never drift and reden thellms.txt mirror in syncquality-gate check on an otherwise-clean PR (same gitignore-and-generate fix asmcp/src/generated/llms-txt.ts#1928 andGeneratedDemos.kt#1976);check-llms-drift.shnow enforces the structural invariant that the mirror stays untracked. Separately,sceneview-web'sSCENEVIEW_VERSIONconstant and itsSceneViewVersionTest.ktregression pin are now swept and auto-fixed bysync-versions.sh, so a release version bump no longer leaves the constant stale (shipping a wrong version) and the:sceneview-web:jsTestjob red. - Flutter iOS AR bridge:
clearScenenow removes placed models and tap placements no longer leakAnchorEntitys (#2078). The Flutter plugin's iOS AR placement (ARPlacementControllerinSceneViewPlugin.swift) previously added a freshAnchorEntityto theARViewscene on every plane tap and never removed any — 100 taps left 100 anchors retained — andclearSceneonly dropped the load cache, leaving every tap-placed model on screen permanently. The bridge now mirrors the React Native AR bridge's design: a single reusable contentAnchorNodeis captured once viaonSessionStarted, every tap-placed model is added as its child, and aclearScene(sync(to: [])) callsremoveAll()on that anchor so placed models are actually torn down and the scene's anchor count stays bounded. - React Native iOS: a superseded model load no longer leaks a stale model into the scene (#2079).
RNSceneViewContent.loadModels()andRNARSceneViewContent.placeModels()are driven by SwiftUI.task(id:), which cancels the in-flight task whenever the JSmodelNodesprop changes. A cancelled task still resumes past itsawait ModelNode.load(_:), so the old code could insert a now-stale model into the scene after the prop had already moved on. Both closures now re-checkTask.isCancelledimmediately after everyawaitand bail out before mutating the scene, matching the cancellation discipline already used by the Flutter 3D bridge. - iOS/macOS demo app marketing version stuck at 4.9.0 (#2085). The
samples/ios-demoXcode project'sMARKETING_VERSION(which drivesCFBundleShortVersionString) was frozen at4.9.0, so every iOS and macOS build since v4.9.0 reported marketing version4.9.0to the App Store regardless of the real SDK version — the release pipeline only bumped the build number (CURRENT_PROJECT_VERSION). Both build configurations of theSceneViewDemoapp target are now at the source-of-truth version, andsync-versions.sh--fixrewritesMARKETING_VERSIONin lockstep withgradle.propertiesVERSION_NAMEso a future release bump sweeps it automatically and it can never drift again. - Damaged Helmet renders all-black across demos (#2087). The bundled
samples/android-demo/.../models/khronos_damaged_helmet.glbcarried its base-color and emissive textures as WebP-encoded images. Filament's glTF loader (gltfio) decodes embedded textures with stb_image, which does not support WebP, so the base-color texture silently fell back to a black 1×1 placeholder — and with the helmet material'smetallicFactor = 1the model collapsed to an all-black blob in every demo that loads it (Model Viewer, Lighting, Camera Controls, Environment, …). This also produced unusable Play Store screenshots. The two WebP textures have been re-encoded to JPEG in place; geometry, nodes, samplers and material parameters are untouched. The helmet now renders with correct PBR shading. (The web-demo copy was already JPEG and unaffected.) - Relocated the tablet Play Store screenshots added by #2092 from the
orphaned
samples/android-demo/play/listings/tree (dead since #1710) into the canonical, CI-syncedsamples/android-demo/distribution/play-store/en-GB/graphics/directory, renamed totablet7-screenshot-*.png/tablet10-screenshot-*.pngsoplay-store.yml's listing-sync actually uploads them. Removed the re-created deadplay/tree, including the 3 Chromebook captures — the Playedits.imagesAPI has no Chromebook image type, so large-screen devices reuse the 10-inch tablet screenshots.
Docs¶
- Corrected the Android demo count across all docs surfaces (#2049).
samples/README.md,CLAUDE.md,docs/docs/samples.md,docs/docs/try.mdandwebsite-static/index.htmlstated three contradictory totals (14 / 37 / 42) with the AR/non-AR split backwards; they now agree on the authoritative figure derived from the per-demo fragment registry — 59 demos (30 non-AR + 29 AR).docs/docs/samples.mdis also rewritten to describe the current append-onlyDemoRegistryinstead of the obsolete 4-tab / 14-demo structure. - android-demo: Play Store tablet & Chromebook screenshots. Added a generated set of large-screen Play Store listing assets under
samples/android-demo/play/listings/en-US/graphics/— six 16:9 tablet screenshots (2560×1440, used for both the 7-inch and 10-inch listing slots) and three 16:9 Chromebook screenshots (2400×1350). The captures cover the headline surfaces — the Dynamic Sky, Environment Gallery, Model Viewer and Geometry Primitives demos plus the Samples and About tabs — taken on a Pixel Tablet emulator running the bundled-asset demos.
v4.14.0 — 2026-05-21¶
Added¶
- AR Record interpretation: new
ARRecordInterpreter(+rememberARRecordInterpreter()) folds every frame of a replayed AR Record dataset into anARRecordInterpretation— camera trajectory length & extent, tracked-frame ratio with a per-TrackingFailureReasonbreakdown, and discovered plane count & area — turning a record/playback session into a quantified, CI-assertable tracking-quality report (#1441). - Play Store CI observability. A new
play-vitals.shrelease-gate (wired intorelease-checklist.shsection 15) grades the real-world crash & ANR rate from the Play Developer Reporting API — advisory by default, blocking underPLAY_VITALS_HARD=1(#1691). A new dailyplay-reviewsjob inmaintenance.ymlingests Play Store ratings + reviews via the Android Publisher API and auto-opens a de-duplicated triage issue for any review matching a crash/bug signal (#1692). Both reuse the existing deploy service account read-only — no new write scope. - People Occlusion —
ARCameraStream.isPersonOcclusionEnabledoccludes virtual objects behind real people using ARCore Scene Semantics'PERSON-class segmentation mask (flagship parity with ARKitARFrame.segmentationBuffer, AR FoundationAROcclusionManager). Newcamera_stream_person_occlusion.filamatcamera material (a strict superset of the depth-occlusion material) and anar-people-occlusiondemo. RequiresConfig.SemanticMode.ENABLED; outdoor scenes only (#1761). - Body tracking on Android via MediaPipe Pose (#1763): a new
io.github.sceneview.ar.bodypackage inarsceneviewships renderer-agnosticBodyPose/BodyLandmarkvalue types and a 17-jointJointenum named to match ARKit'sARSkeleton.JointNamefor cross-platform parity.BodyPose.fromMediaPipeLandmarks(...)projects the 33 raw MediaPipe Pose Landmarker landmarks onto the joint set (synthesisingROOT/SPINE/NECKas anatomical midpoints), andSKELETON_BONESexposes the bone topology for overlays. A newar-body-trackerdemo insamples/android-demoruns Google's on-device MediaPipe Pose Landmarker on the AR camera feed and draws a live 2D skeleton overlay. Honest parity note: ARCore has no native body-tracking API, so unlike ARKit'sARBodyTrackingConfiguration+BodyTrackedEntitythis is image-space tracking (normalised pixel coordinates + relative depth), not a world-anchored 3D skeleton — ideal for 2D overlays, fitness/gesture detection and AR filters, but not a drop-in for a world-anchored rig. The MediaPipe runtime stays a sample-only dependency; the publishedarsceneviewartifact carries only theBodyPose/Jointvalue types. - Collaborative AR — multi-user sessions. New
io.github.sceneview.ar.collaborativepackage brings shared-coordinate-frame multiplayer to ARCore.CollaborativeSession(and the lifecycle-boundrememberCollaborativeSession()helper) orchestrates a shared AR experience on top of the existingCloudAnchorNode: one device hosts the shared Cloud Anchor, every other resolves the same id, and participant camera poses + placed-node transforms are relayed between peers as JSON-lines messages. The networking layer is a pluggableCollaborativeTransportinterface — SceneView deliberately does not pick a stack — shipped alongside an always-available, no-networkingLoopbackCollaborativeTransportreference impl that makes the API unit-testable and demonstrable on a single device.CollaborativeWireFormatis pure Kotlin with zero new runtime dependencies, and the whole merge core (CollaborativeState, last-writer-wins) is covered by 52 JVM unit tests. The newar-collaborativesample demo proves the full sync end-to-end without a second phone. ARCore has nocollaborationDataAPI (unlike ARKit) — this is the honest, buildable shape of multi-user AR on Android. A production Nearby Connections transport is filed as a follow-up (#1764). - In-app feedback — the Android demo app now has a "Feedback" button on every tab: users record their screen + voice to report a bug or share an idea, the recording is transcribed server-side and filed as a pre-filled GitHub issue, and a "My feedback" screen tracks each submitted ticket's live Open/Closed status with a tap-through to the real issue. (#1930)
- In-app feedback — screen + mic recording (1C): the Android demo app captures a screen recording with microphone audio via
MediaProjectionand amediaProjectionforeground service, demuxes the AAC audio track into a standalone file for server-side transcription, and shows a review screen (duration, optional note, record-again / send) before the recording is submitted. Recording is optional for the "Idea" category. (#1933) - In-app feedback — upload & context capture (1D): the Android demo app uploads each feedback submission as a multipart
POSTto the feedback worker, with a determinate progress bar and graceful retry on failure. The submission carries an automatic context snapshot — app version, Android version, device, locale, free RAM, and the exact demo / navigation route the feedback is about — and a confirmation screen shows the created GitHub issue number with a tap-through link. The worker base URL is a single configurableBuildConfigfield (FEEDBACK_WORKER_URL). (#1934)
Changed¶
- Secondary Camera (PiP) demo: added an Orbit chip that flies the picture-in-picture camera around the model on its own, independently of the user's main-view orbit. This makes the per-instance
cameraNodebinding visibly independent — one scene, two cameras moving on their own — instead of just parking the PiP at a fixed angle (#1256). - Consolidated the two lighting demos into one (#1444). The Android demo's
lighting("Light Types") andmovable-light("Movable Light") cards were near-identical — same helmet model, same topic — so they are merged into a single Lighting demo with an in-demo segmented-button mode switch: Light Types (directional / point / spot, intensity, colour) and Movable Light (drag to orbit the light). No feature is lost — every control from both demos is still present. The Samples tab now carries one lighting entry instead of two. The retiredsceneview://demo/movable-lightdeep link keeps working:DeepLinkRouteraliases it tolightingvia a newDEMO_ID_ALIASEStable. - Demo app:
DemoScaffoldnow exposes an opt-inonResetparameter that renders a consistent, always-in-the-same-place Reset action in the demo's top app bar, giving every demo a predictable path back to its initial state and re-arming its core interaction. A brief confirmation snackbar ("Demo reset — ready to try again") tells the user the demo is ready for re-interaction. Wired into the owner-flagged AR Depth Occlusion demo. (#1966)
Fixed¶
- Post-Processing demo now makes SSAO visibly flagrant (#1443). The damaged-helmet model is staged sitting on a plain matte ground plane instead of floating in the void, and the camera is raised to an angle that frames the floor. SSAO darkens the contact zone between the helmet and the plane, so toggling the SSAO switch now makes a soft contact shadow plainly appear and disappear — the post-processing difference reads at a glance instead of being a subtle change easy to miss.
- Cloud Anchors demo: renamed the setup runbook
STREETSCAPE_SETUP.md→ARCORE_CLOUD_SETUP.mdso a Cloud Anchor demo no longer routesERROR_NOT_AUTHORIZEDusers to a Streetscape-named doc, and updated all 14 references across the demos,arsceneview,build.gradleandllms.txt(#1614). The on-screen Host/Resolve actions already shipped viaSceneActionBarin #1986. - In-app feedback (Android demo): hardened the screen-recording feedback feature after a review — capped the recording so a long clip can no longer 413, added an in-demo feedback entry point, fixed the demo-id context key, made the upload error messages specific, and survived process death mid-flow (#1930).
- feedback-worker: closed security + correctness blockers from review — enforce the 30 MB upload cap before buffering the body (streaming guard +
Content-Lengthvalidation), SHA-256 IP hashing in the rate limiter, fenced-code Markdown rendering of user text/transcripts on the public issue, base64 Whisper input verified + multi-MB-safe, orphaned-R2 cleanup on D1 failure, incremental retention cron, cached GitHub installation token, and admin-token brute-force rate limiting (#1930). - CI Gate stopped failing every PR (#2013). The
CI Gateworkflow shelled out to.github/scripts/ci-gate-aggregate.shwith noactions/checkoutstep, so the helper was never on disk and the gate died with "No such file or directory" on every PR — the real cause of the v4.13.0 admin-merge spree. A checkout step was added. The aggregator also now drops advisory checks (e.g.Coverage (advisory)) from its pending-wait set, not just from the failure verdict, so a slow or hanging advisory job can no longer push the gate past its deadline. - Release / docs workflows survive a failed Pages-rebuild trigger (#2014). A non-
201response from the GitHub Pages build API (e.g. an expiredPAGES_REBUILD_TOKENreturning HTTP 401) is now a loud warning instead of a hard failure — the release/docs artifacts already published, so the auxiliary rebuild trigger must not fail the run. - Honest capability badges for the Flutter/RN bridges (#909). The Flutter and React Native demo apps and READMEs no longer over- or under-state what the bridges actually expose. The Flutter demo's About tab gains a tri-state "Bridge Coverage" list (Android + iOS / Android only / Not yet bridged), the RN demo's AR tab labels
depthOcclusion/instantPlacementas "Not yet bridged" since those props are accepted but never applied to the ARCoreConfig, and every README now carries a coverage map. Stalev3.6.1version strings in the Flutter demo were corrected. verify-sketchfab-key.sh: droppedcurl -ffrom the live API probe so the real HTTP status reaches thecase— the401|403"token revoked" branch was unreachable and a revoked Sketchfab key silently passed the release guard.docs.yml: ref-scoped the workflow concurrency group (pages-${{ github.ref }}) so a release tag's two triggers (push+release) no longer self-cancel mid-deploy, while same-ref dedup is preserved.- Removed the dead
.github/scripts/ar-emulator-screenshots.sh— it had no caller anywhere in the repo. telemetry-ci.yml: addedbranches: [main]to thepush:trigger so it no longer runs on every branch push.check-workflow-scripts.sh: now scans every workflowif:expression and fails on a context disallowed inif:(notablysecrets) — the class of invalid-if:bug behind the v4.13.0 release startup-failure.collate-changelog.sh: the preamble splice now keeps every line before the first##section instead of emitting only line 1, so intro prose between# Changelogand the first section is no longer dropped on release.- Fixed the release pipeline: the
secretscontext is not allowed in a GitHub Actions stepif:expression, which maderelease.yml(anddocs.yml) invalid workflow files and blocked the v4.13.0 publish. The token-presence check is now done inside the step'srun:script.
Tests¶
- Device-QA screen recording moved to the host-side emulator console (#1671). New
android_cli_screenrecord_*helpers useadb emu screenrecord, which is immune to the Emulator 36.x gfxstream regression that recorded-gpu hostFilament content as near-empty — so the QA emulator drops the 35.6.11 version pin and runs the latest emulator. - Android demo QA — emulator boot snapshots.
setup-ar-emulator.shgains--seed-snapshot/--no-snapshot: a clean post-ARCore-install boot snapshot (qa-clean) is seeded once and cold-booted from on every subsequent QA run with-no-snapshot-save, so runs start from an identical warm state and the AVD userdata partition no longer degrades after ~6 runs. Faster, deterministic local QA. Android Studio Journeys was assessed but deferred — it requires an AGP 9.0.0 bump (#1672). - Web device-QA WebXR coverage now drives a full
immersive-ar/immersive-vrsession against the IWER emulated device — requests the session, runs the XR animation frame loop, nudges pose/controllers and ends it — replacing the fixture-pending soft-skip, so a WebXR-plumbing regression fails the suite instead of silently skipping (#1674, #1748). - Device-QA: the Android leg now screen-records each run via host-side
adb emu screenrecord, completing cross-platform parity with the iOS (simctl io recordVideo) and web (Playwrightpage.screencast) legs. Host-side capture is immune to the Emulator 36.x gfxstream regression that recorded-gpu hostFilament content as near-empty. The Android and iOS QA recordings are now surfaced intodevice-qa-artifacts/alongside the web screencasts. - Added a non-AR demo regression suite — pure-JVM state-machine tests for
AnimationDemo's cinematic camera scripts and a demo-registry integrity check — plussamples/android-demo/DEMO_TESTING.mddocumenting the three test layers (#880).
Docs¶
- Privacy disclosures updated for the opt-in in-app feedback feature: the demo app's privacy policy (
.github/PRIVACY_POLICY.md,docs/docs/privacy.md, websiteprivacy.html) now discloses screen + microphone capture, device/app context, Cloudflare Workers AI (Whisper) transcription, private Cloudflare R2 storage, the 90-day retention window, and that a public GitHub issue carries only the transcript + context. Adds a Play Store Data safety reference doc (samples/android-demo/distribution/play-store/DATA_SAFETY.md) for the maintainer to transcribe into the Play Console. (#1935) llms.txt: added an explicit "Web API model — builder DSL, NOT a Node scene-graph" section to the SceneView Web reference, with a concept-mapping table (Android/iOSNode↔ Web builder DSL) and correct-vs-incorrect code examples. This stops AI assistants from generating Android-styleNode-tree code that does not compile againstsceneview-web, and documents that a node scene-graph for Web is a tracked v5 milestone effort (#895).
v4.13.0 — 2026-05-21¶
Added¶
- AR Augmented Images — on-device runtime registration. New
RuntimeAugmentedImageDatabasehelper (rememberRuntimeAugmentedImageDatabase()) lets you register a brand-new reference image at runtime — e.g. from a photo the user just took — without a pre-bundledarcoreimgdatabase.addImage(name, bitmap, widthInMeters)runs the ARCore feature extraction off the main thread and re-applies the session config on the main thread itself, returning a typedAddImageResult(Added/LowQuality/Error) so low-quality captures are recoverable. NewFrame.captureCameraBitmap()andImage.toArgbBitmap()extensions grab the live AR camera frame as an uprightARGB_8888bitmap ready for the database. The Image Tracking demo now ships a "Capture this view" button demonstrating the full on-device flow (#1553). - Record & Playback demo now surfaces live ARCore tracking quality while recording — a status pill, a "tracking lost" soft warning, and a per-take "tracking healthy X% of frames" stat — so a capture going bad (e.g. shot from a moving vehicle) is obvious in real time instead of only on playback (#1650).
- CI: daily
maintenance.ymljob that monitors Android App Links + iOS/macOS Universal Links verification health — cross-checks the hostedassetlinks.json/apple-app-site-associationagainst the committed source of truth and the demo apps' intent-filters/entitlements, opening a tracking issue when the QR → demo deep-link flow is broken (#1695). PlacementScenecomposable (#1765) — one-line tap-to-place AR scene with SceneformArFragmentparity: bundlesARSceneView+ plane rendering + a built-in centre-screen reticle + tap-to-place anchor creation + an instant-placement fallback, so callers only declare what rides each placed anchor. NewPlacement Scenedemo insamples/android-demo.PointCloudNode+rememberPointCloud()(#1773): renders ARCore's live tracking feature points (Frame.acquirePointCloud()) as an in-scene Filament point cloud — AR FoundationARPointCloudManagerparity — with a configurable color and confidence filter. Ships a newPoint CloudAR demo.PlaneNodecomposable +rememberDetectedPlaneslifecycle helper forarsceneview(#1774): react to ARCore detected-plane lifecycle (onAdded/onUpdated/onRemoved) declaratively from Compose — the SceneView equivalent of AR Foundation'sARPlaneManager.planesChanged— instead of hand-rolling aframe.getUpdatedTrackables(Plane::class.java)loop. New "Plane Lifecycle" demo insamples/android-demo.MaterialLoader.createOcclusionInstance()— invisible, depth-writing material (RealityKitOcclusionMaterial/ SceneformmakeOcclusionMaterialparity). Compose helperrememberOcclusionMaterialInstanceships insamples/common. New "Occlusion Material" demo in the Android demo app (Advanced category). For AR scenes that want occlusion against the live depth camera, keep usingARCameraStream.isDepthOcclusionEnabled. (#1776)- Scene Semantics label-overlay material (#1868, follow-up of #1730): a new
semantics_overlay.filamatFilament material colour-codes ARCore's per-pixel 12-class outdoor segmentation, exposed viaMaterialLoader.createSemanticsOverlayInstance(texture, opacity)plusMaterialInstance.setSemanticsTexture/setSemanticsOpacity.ARSceneSemanticsDemonow renders the live segmentation as a camera ↔ semantic blend overlay (with a colour legend) alongside the existing top-3 label HUD. ReticleNodelibrary-level placement reticle (#1882). Newarsceneviewnode +ARSceneScope.ReticleNode { ... }Composable for the "tap to place" UX every AR placement demo previously had to reinvent.ReticleNodeis a thin wrapper overHitResultNode— it delegates the screen-coordinate hit test (including #1891's plane-only defaults and the 30 cmminCameraDistancefloor) toHitResultNodeand adds only theonHitResultChangedcallback so callers can drive an "aim at a surface" hint and capture the last-known hit on tap-to-place without attaching a duplicate hit test inonSessionUpdated. Auto-hide on no-hit comes for free fromHitResultNode's trackable/visibility contract. Visual marker is left to the caller as a child node so the reticle stays material/style-agnostic. Documented inllms.txt(and the docs mirror) +sceneview-mcpbundle.- Jetpack XR hand tracking (Slice 2, #1902): new preview
XrHandNodemirrors anandroidx.xr.arcore.Handas a scene-graph node with one child node per skeleton joint, aSceneScope.XrHandNodecomposable, the JVM-testableXrHandSkeletonjoint/bone math, and anar-hand-trackingdemo that renders a static reference skeleton on non-XR phones. XrFaceNode— Jetpack XR face tracking (androidx.xr.arcore.Face) for Android XR headsets, the preview sibling ofAugmentedFaceNode, plus the runtime-freeXrFaceMeshadapter and anar-xr-facesample demo (#1903).
Changed¶
- AR plane visualization redesigned — the dated dense dot-grid overlay is replaced by a modern procedural soft grid with anti-aliased lines and a feathered edge fade (#1616).
- AR Pose Placement demo now places a real bundled Lantern model instead of a placeholder cube/sphere and shows the live X/Y/Z coordinates as in-scene text. (#1618)
- Demo app UX-consistency pass (#1620 thread 1): dropped low-value Settings sheets — demos with no real controls (
OrbitalARDemo,ARMLObjectLabelDemo) no longer show a Settings FAB, AR demos whose sheet only held the dev-onlyForceTrackingFailureMenu(ARStreetscapeDemo,ARImageDemo,ARSceneSemanticsDemo) now show the FAB only in QA mode, and the verbose "How to test" help cards inARDepthOcclusionDemo/ARImageStabilizationDemowere trimmed to a one-line hint so the sheet is just the real toggle. Status/device-support text that was buried in sheets is now surfaced on-screen. Consolidated the duplicate Play Store listing directories into a singlesamples/android-demo/distribution/play-store/en-GB/source of truth (text +graphics/), and extended theplay-store.ymllisting-sync to upload the feature graphic and screenshots via the Playedits.imagesAPI so they reach the store automatically on release (#1710). - Upgrade detekt 1.23.8 (silently no-op'd on Kotlin 2.3.x) → detekt 2.0.0-alpha.0; per-module baselines committed under
buildSrc/config/detekt/baseline-<module>.xmlgrandfather existing violations and theDetektCI step is now blocking on NEW violations (#1740). - Release builds of the demo apps now fail loud when
SKETCHFAB_API_KEYorARCORE_API_KEYis empty (#1915): the AndroidassembleRelease/bundleReleasepath and the iOSReleasearchive abort with a clear actionable error instead of silently shipping a store build with invisible Sketchfab carousels (the #1909 silent-fail class). Debug builds stay permissive; forks opt out withSV_ALLOW_MISSING_SECRETS=1. - Pruned the unused
focusPoint/radiusspotlight parameters fromplane_renderer.mat(#1922): these declared a half-built "spotlight around the focus point" effect whose fragment-shader falloff andPlaneRenderer.ktsetter were both already commented out, so they never affected rendering. The.matsource, the orphaned Kotlin constants/getFocusPoint(...)helper, and the regeneratedplane_renderer.filamatblob are all updated together — no behaviour change. - CI: split JaCoCo coverage off the PR-blocking unit-test job (#1955) — the blocking
Unit testsjob now runs the plaintestDebugUnitTestsuite (fast, deterministic, 30-min timeout), while JaCoCo instrumentation + reports run in a separate non-blockingCoverage (advisory)job, so a slow runner can no longer push the unit-test gate over its timeout and turnCI Gatedouble-red. - Demo app: enforced one action-placement rule — every demo's primary action (Host, Drop, Place, Record, Clear, Reset) is now an on-screen button via the shared
SceneActionBar, while only secondary configuration (toggles, pickers, the Cloud Anchor ID field) stays in the Settings sheet (#1964). - Added labelled "Camera distance" sliders to the
CameraControlsDemoandModelViewerDemoAndroid demos (#1965): zoom was pinch-only — now discoverable and Maestro-testable (no pinch in Maestro) — and the sliders complement pinch-to-zoom rather than replacing it.ModelViewerDemo's slider drives the sameDemoSettings.cameraDistancedeep-link hook (#1571). - Build:
samples/android-demo'sGeneratedDemos.ktis no longer committed — it is.gitignored and regenerated before Kotlin compilation by the newgenerateDemoRegistryGradle task, killing the per-PR merge-conflict class that hit every demo-adding PR (#1976).
Fixed¶
- Streetscape Geometry demo now surfaces clear "go outdoors" guidance after 15 s with no geometry, instead of spinning forever on "Looking for streetscape geometry…" indoors (#1615).
- Depth occlusion now actually occludes (#1617):
ARCameraStreamdraws the depth-aware camera quad first (Filament priority 0) when occlusion is enabled so the real-world depth written viagl_FragDepthprimes the z-buffer before virtual geometry is depth-tested — previously the quad was always drawn last (priority 7), writing real-world depth too late to ever hide a virtual model behind real furniture. - AR demos + docs polish (#1777):
ARDepthOcclusionDemonow shows a transition spinner while the depth toggle rebuilds the camera stream (+ aconnectedAndroidTestthat flips depth mode 10× and asserts stability);LightEstimatorgains anenableColorCorrectiontoggle and exposes the rawlastColorCorrectiontriple;sessionConfiguration/sessionCameraConfigKDoc now warns about mid-session config swaps;llms.txtdocuments camera-config swapping and editable nodes (TransformableNodeparity). - Fix
sceneview.github.iono longer rebuilding on push: GitHub Pages' legacy auto-build does not fire for the SSH deploy-key pushesdocs.yml/release.ymlmake, and the existing "Trigger GitHub Pages build" workaround was permanently skipped because itsif:condition tested anenv:var set on the same step (not yet in scope) and referenced a non-existent secret. The step now testssecrets.*directly, falls back to the existingPERSONAL_TOKEN, and fails loudly on a bad API response; a dailymaintenance.ymljob alerts if the live site lags its source by more than a day (#1826). - Spatial Audio demo (Android): the bouncing sphere is now clearly visible — larger radius, brighter on-brand material, closer camera framing, and a two-light key/fill setup. (#1927)
- In-app update flow in
samples/android-demois now demo-UI-native (#1941):InAppUpdateManager.checkForUpdate()no longer auto-starts the Google Play consent modal on resume — it only surfaces an integrated Material 3UpdateBanner("A new version is available"). A newInAppUpdateManager.startUpdate()triggers Google's single consent dialog, called solely on the user's deliberate tap of the in-app "Update" button. This fixes the double-modal (a secondonResumein theAVAILABLEwindow is now a no-op), the "feels like leaving the app" jarring unprompted popup, and the flaky "Restart" button —completeUpdate()is now a no-op unless the install isREADY_TO_INSTALLand the install-state listener stays registered untilINSTALLED. - Hardened the demo apps' in-app update flow (#1942 follow-up): cancelling Google's flexible-update consent modal is now delivered via an
ActivityResultLauncher(startUpdateFlowForResult), so a cancel resets the banner to a retryable state instead of stranding the Update button; the in-app update info is kept until the download is confirmed started so a cancelled flow can be retried; adestroyedguard stops late Play Core callbacks from mutating state afteronDestroy; the manager re-attaches to an already-running download after a rotation; and the Android TV banner's new "Update" button is now reachable by D-pad. - Web Spatial Audio demo Play button now actually plays (#1944): the
samples/web-demoSpatial Audio panel's#audio-play/#audio-stopbuttons were dead — clicking Play produced zero Web Audio API activity. The wiring lived only in the Kotlin/JSMain.kt::setupSpatialAudio(), butindex.htmlships the hand-writtenjs/sceneview.jsruntime, not the Kotlin/JS bundle, so that code never executed. The buttons are now wired in the inline-JS runtime alongside the other tab demos: a click constructs theAudioContext(inside the user gesture, per the autoplay policy),fetch+decodeAudioDatas the bundledaudio/bell.wav, builds theAudioBufferSourceNode -> PannerNode("HRTF") -> GainNodegraph, starts looping playback, and orbits the panner around the listener — matching the Android/iOS Spatial Audio demos. A new Playwright regression test (tests/audio.spec.ts) hooks the Web Audio API before page load and asserts the graph is constructed on a real#audio-playclick, so CI catches a future regression. - Post-v4.12.0 audit polish (#1957):
OpenGL.createEglContext()now reports a descriptiveEGL context creation failederror instead of a bare!!NPE;cameraConfigFilter { }gains scalartargetFps(…)/depthSensor(…)/stereoCamera(…)convenience functions so single-sensor filters no longer needsetOf(…)(theSetAPI from #1844 is unchanged); malformedGeometryvertex lists with partial attribute declarations now fail with a named-attribute error instead of an opaque render-time NPE; and two edge cases gained regression tests —DepthMeshNode.computeAabbwith an identical-Z depth frame, and theFrame.hitTestDepthzero-width / zero-focal-length intrinsics guard. - The
.well-known/deep-link manifests (assetlinks.json+apple-app-site-association) are now deployed to the live site — the website assembly step'scp website-static/*glob silently skipped the dot-prefixed directory, so Android App Links / iOS Universal Links auto-verification returned HTTP 404 (#1998). - CI workflow hardening (#1702, #1708, #1984): the
CI Gateaggregator no longer red-lights a PR when an advisory check (e.g.Coverage (advisory)) isCANCELLED/SKIPPED/FAILURE— the pass/fail decision now excludes any check whose name matches anADVISORY_CHECKSsubstring, for all conclusions (a transient concurrency-cancel of advisoryCoveragehad blocked otherwise-mergeable PR #1889 for ~3h). The decision logic is factored into.github/scripts/ci-gate-aggregate.shwith a regression suite (test-ci-gate-aggregation.sh, wired intoci.ymlrepo-hygiene) covering the CANCELLED-advisory case. Also confirmed and pinned:docs.ymluses matchingupload-artifact/download-artifact@v7pairs (no@v8mismatch), andci.yml'squality-gatejob uses the sharedsetup-gradlecomposite action so it gets thegradle-wrapper.jarSHA validation supply-chain guard like every other Gradle job.
Removed¶
- Removed the dead Kotlin/JS source set of
samples/web-demo(#1946): the web demo'ssrc/jsMain/.../Main.ktbuilt aweb-model-viewer.jsbundle thatindex.htmlnever loaded — the shipped page has always run on its hand-written inline<script>+ self-hostedjs/sceneview.js. The deadMain.kt/WebXRParityDemos.kt, the Kotlin/JS Gradle wiring (build.gradle.kts,webpack.config.d/, the:samples:web-demosettings.gradleinclude) are gone; the static deliverable moved fromsrc/jsMain/resources/tosamples/web-demo/site/. The web demo is now a plain static site with one source of truth (the inline JS) —docs.ymldeploys it with a verbatim file copy and the Playwright suite serves it directly. Root cause of #1541 and #1944.
Tests¶
- QA:
web-perf-qa.shnow enforces a tuned Lighthouse perf budget (mobile preset — FCP/LCP/CLS + perf-score) instead of always emitting an advisory verdict, anddevice-qa.shrecords the result as an advisoryweb-perfleg so a budget breach surfaces indevice-qa-report.json's release gate (#1898, follow-up of #1879). - Registered the new
occlusion.mat/occlusion.filamat(added by #1832) in thetools/GenerateFilamat.shinventory under a new Profile E (-a vulkan -a opengl -p mobile), so the.filamatABI drift guard now covers all 21 material blobs (#1949).
Docs¶
- Added a "Hand / Face / Body tracking parity" table to the iOS cheatsheet mapping mobile ARCore, Jetpack XR (
XrHandNode/XrFaceNode), ARKit phone, visionOS, and WebXR — and cross-linked it from the Jetpack XR integration design notes (#1904). - Removed the stale
website-static/llms.txt(pinned at v4.0.9, 12 versions behind the canonical rootllms.txt) so the deployedsceneview.github.io/llms.txtalways serves the current API reference (#1956); added cross-platform parity rows for v4.12.0 Spatial Audio (#1900) and Haptic Feedback (#1901) to the iOS and Android cheatsheets, stating the real iOS / Web maturity (#1958). - Corrected the stale node-type count across
README.md, the website, structured data, docs, and the MCP docs to the actual 41 (24 3D + 17 AR) — previously claimed 35-39 and missed the ARPointCloudNode,PlaneNode, andReticleNodeadditions.
v4.12.0 — 2026-05-21¶
Added¶
- Auto-fit camera framing (#1439): a new library-level helper in
io.github.sceneviewcomputes the orbit distance at which a model's bounding sphere exactly fills the viewport, regardless of the model's intrinsic glTF size.fitDistanceForBounds(bounds, verticalFovDegrees, aspect, padding)is pure trigonometry (yaw-invariant — fits the bounding sphere, not the raw box);CameraNode.frameToContent(node)/CameraNode.frameToBounds(aabb)reposition the camera in one call;verticalFovDegreesForFocalLengthandBox.toAabb()convert Filament's focal-length /Boxtypes;SceneAutoFitStateis a one-shot guard for use in aSceneViewframe loop. The Model Viewer demo now auto-fits its orbit radius to the displayed model — a 5 cm bee and a 5 m crate are framed identically without per-demoscaleToUnitstuning. Android-only for now; iOS already frames fromvisualBounds(#1026 / #1391). arsceneview: Environment-aware AR fog —ARFogNode(inio.github.sceneview.ar.node) blends the live camera passthrough toward a coloured haze using the ARCore depth image, so distant real-world surfaces fade while near ones stay crisp. MirrorsFogNode'sdensity/color/enabledparameters so the same numbers fog both real and virtual geometry visually consistently, plus AR-onlystart/enddistance bounds. Inspired by ARCore Depth Lab's AR Fog sample. Opt-in, off by default — collapses to a no-op whenenabled = false(zero shader cost via a branchlessfogEnabledgate). RequiresConfig.DepthMode.AUTOMATIC(orRAW_DEPTH_ONLY) andARCameraStream.isDepthOcclusionEnabled = true. The depth-aware camera material (camera_stream_depth.mat) was extended with the fog term and its.filamatblob recompiled with the matching matc 1.71.0 toolchain — seeCONTRIBUTING.md. Demo: newARFogDemoinsamples/android-demo(deep linksceneview://demo/ar-fog), with sliders that drive both the real-world fog and a virtualFogNodein lockstep so the parity is visible side-by-side (#1717).- New
ARMLObjectLabelDemoin the Android demo app — ML Kit object detection on the AR camera feed, with 3D billboard labels anchored at detected real-world objects via depth hit-tests. Uses the bundled offline ML Kit model (com.google.mlkit:object-detection), so the demo works without any extra asset download. Ships alongside a newFrame.cameraImage()extension onarsceneviewexposing the YUV CPU image for ML / CV pipelines. (#1737, #1733) arsceneview: surfaced 11 ARCoreConfig.*Modeenums as typed DSL params onARSceneView—planeFindingMode,depthMode,instantPlacementMode,geospatialMode,streetscapeGeometryMode,cloudAnchorMode,augmentedFaceMode,imageStabilizationMode,semanticMode,updateMode,focusMode. Each defaults to ARCore's recommended value, is applied to theConfigBEFORE thesessionConfigurationcallback (so the callback still wins as an escape hatch), and is reactive — flipping a param via Compose state reconfigures the running session without recreatingARSceneView. Demos (ARCloudAnchorDemo,ARInstantPlacementDemo,ARPlacementDemo) migrated off the rawsessionConfigurationcallback (#1766).- sceneview-web: WebXR feature parity composables —
XRDepthInfo+DepthOcclusionShader(depth-sensing),XRHandNode(handedness).joint(Joint.INDEX_TIP) { ... }(hand-tracking, 25 joints),XRImageTrackingNode(index = 0)(image-tracking with the newXRFeature.IMAGE_TRACKINGconstant),XRAnchorNode(xrAnchor)(anchors). Mirrors the Androidarsceneviewcomposables.XRFramegains thegetDepthInformation(view)andgetImageTrackingResults()extensions plus atrackedAnchorsaccessor (#1778, part of #1754). - New
SpatialAudioNode(Android + iOS + Web) — positional 3D audio attached to scene nodes with inverse/linear distance falloff. Each node owns its own player so two nodes never cross-talk. Android phase-1 per-nodeMediaPlayerbackend (Spatializerin phase 2); iOS RealityKit spatial audio; Web AudioPannerNodeHRTF. Phase 1 of #1900 — drive the listener withsetSpatialAudioListenerPose(position, forward, up)from the render loop; automatic camera tracking is phase 2. - New
rememberHapticFeedback()(Android) +SceneViewHaptic(iOS) + Webnavigator.vibratefallback. 7 presets (light/medium/heavy/success/warning/error/selection) +continuous()+pattern()+cancel().continuous(intensity, durationMs)takes a millisecondInton every platform — Android, iOS and Web — so cross-platform callers pass the same value. Library API replaces ad-hoc per-demo wrappers. Phase 1 of #1901; NodeGesture modifiers + AR event modifiers come in phase 2 / phase 3. - Filament materials CI guard (#1912 Part B):
tools/GenerateFilamat.shis rewritten to resolve the pinned matc version fromgradle/libs.versions.toml, download + cache the matched matc tarball under~/.cache/sceneview/matc-<version>/, and compile every.matsource with its profile-specific flag list. A new--checkmode regenerates each blob to a tmp dir and byte-diffs against the committed.filamat, exiting non-zero on drift; the gate is wired into.claude/scripts/quality-gate.shso PRs that ship a.matedit without a matching.filamatrecompile are now blocked automatically. The smoke recompile surfaced and fixed threewebsite-static/blobs that were still compiled against matc 1.70.x while the runtime moved to Filament 1.71.0.
Changed¶
- API consistency polish (#1844). Tier-2 Wave-4 follow-ups bundled into one release surface:
ARSceneView(onSessionFailed = …)soft-deprecated in KDoc in favour of the typedonSessionFailure(#1759). Both still fire when set; the legacy callback stays available indefinitely for backwards compatibility.rememberARPlaybackStatusported to theproduceStateidiom — matchesrememberCameraGeospatialPose/rememberEarthStateinstead of the bespokeLaunchedEffect + mutableStateOfpair.ARSceneScope.DepthHitResultNodeadds a customhitTest: (Frame) -> DepthHitResult?lambda overload — mirrorsHitResultNode's 2-overload surface. Apps wanting multi-pixel / moving-reticle depth selection no longer have to subclass the node.cameraConfigFilter { … }DSL:depthSensorandstereoCameraare nowSet<…>?instead of singletons — symmetric withtargetFpsand with the underlying ARCoreset*(EnumSet)API.setOf(X)keeps the singleton case ergonomic. Empty sets fail fast (validation moved out of #1845).- Cheatsheets refreshed:
docs/docs/cheatsheet.mdabsorbs the Wave-4ARSceneView(onSessionFailure / playbackDatasetUri / flashMode)parameters;docs/docs/cheatsheet-ios.mdlists every new Android-only API surface so AI agents stop emitting iOS code referencingARSessionFailure,DepthHitResultNode,cameraConfigFilter,Frame.cameraImage(),rememberARPlaybackStatus, orARRecorder.addTrack / recordTrack / State.IO_ERROR. - Changelog fragments gain a
Performancecategory — covers pure perf wins (#1810-style) that don't fitFixedorChanged..claude/scripts/collate-changelog.shrecognises it. mcp/src/generated/llms-txt.tsis now build-generated, not committed (#1928). The ~230 KB embeddedllms.txtbundle is.gitignored and regenerated by theprebuild/prepare/testnpm lifecycle scripts, removing the guaranteed merge conflict every parallel PR that touchedllms.txtused to hit. The publishedsceneview-mcptarball still ships the compileddist/generated/llms-txt.js.
Fixed¶
- AR demos render placed PBR models as flat-black silhouettes (#1611). Two fixes to the
ARSceneViewIBL path. (1) The baselineenvironment.indirectLightis now applied viaLaunchedEffect(environment)instead ofSideEffect— demos that surface per-frame ARCore state to UI state (latestFrame,isTracking) used to recompose every frame and silently resetscene.indirectLightback to the baseline, dropping the per-frame rebuilt IBL produced by ARCore'sENVIRONMENTAL_HDRestimate. (2) The per-frame rebuild is now gated byshouldRebuildIndirectLight(estimation, baseIndirectLight)so partial estimations (e.g. reflections cubemap in flight, irradiance SH not yet stable) skip the rebuild instead of producing an IBL with empty irradiance OR empty reflections — KTX1-loaded baselines expose SH via the native handle (noirradianceTexture), so the legacy fallback returned a no-IBL builder and Filament collapsed diffuse PBR to black. Pinned by 4 new pure-JVM cases inIndirectLightRebuildDecisionTest. Verified on Pixel 9: placed Damaged Helmet, Fox, Lantern, Toy Car, Shiba and streamed Sketchfab models now render lit on first frame instead of as flat silhouettes. DepthMeshNode.uploadGeometryno longer caches the directByteBufferused forVertexBuffer.setBufferAt/IndexBuffer.setBuffer. Filament's JNI captures a global ref to the buffer and copies ASYNCHRONOUSLY on the render command stream; reusing the buffer across uploads (introduced by the #1810 perf opt) could clobber bytes Filament had not yet consumed → torn vertex uploads, corrupted mesh frames. Reverted to a freshByteBuffer.allocateDirect(...).order(nativeOrder())per upload — matches the standard pattern used by every other Geometry.kt caller. A follow-up issue can revisit the perf cost with a callback-based ring buffer if profiling shows the GC churn matters in practice. Closes #1841.- Sketchfab missing-API-key defensive layer (#1909). When the release build ships with a blank
SKETCHFAB_API_KEYsecret, the Android + iOS demos now surface a neutral "Sketchfab carousels disabled — API key missing" banner in the Explore tab (with a tap-to-explain dialog) instead of silently rendering empty carousels + a dead search bar. CI release pipelines (build-apks.yml,play-store.yml,app-store.ymliOS + macOS archives) fail-fast on tag pushes when the secret is empty/blank or rejected byGET /v3/me, via the new.claude/scripts/verify-sketchfab-key.sh(mirrors the existing ARCore-key guard from #1177). Debug builds also emit a single Logcat /os.LoggerWARN pointing at thelocal.properties/ scheme env-var workaround. Note: the actual secret rotation that re-enables Sketchfab features in shipped releases is a manual GitHub Secrets action — this PR delivers the defensive layer so a future regression is loud at CI time and visible to users. .claude/scripts/quality-gate.sh: TheLARGE_FILEScheck no longer aborts the whole quality gate underset -euo pipefailwhen staged files are under the 10 MB threshold (the common case). Restructured the per-file&&chain into nestedifblocks so the size comparison returning false stays local to the loop iteration instead of propagating throughpipefailand bailing the script viaset -e. Local pre-push runs now reach the finalQuality Gate Summaryblock as intended (#1914).- Filament materials audit Part A (#1918): removed the orphaned
view_renderablematerial — its.matsource and the ~114 KBview_renderable.filamatblob shipped in every APK despite no code path ever loading it (superseded byview_texture_lit/view_texture_unlit). The static audit of all 21 material sources confirmed no dangling parameter reads (no Kotlin call references a parameter the.matdoes not declare) and no leakedMaterialInstances — everycreateInstance(...)is tracked byMaterialLoader,ARCameraStream, orPlaneRendererand destroyed on teardown. Addedwebsite-static/materials/README.mddocumenting the deliberate web-vs-Android divergence, and reviewed the A-vs-B matc flag-profile split as intentional.
Tests¶
- Web device-QA:
assertRendered()incatalog.spec.tsand the non-blank check inrender.spec.tsare now HARD failures (no more soft-warn). Two complementary signals must hold: WebGL context alive (gl.isContextLost() === false) and compositor screenshot shows non-flat luminance variance. Combined with the--enable-unsafe-swiftshaderChromium flag landed earlier, this closes the green-on-nothing risk on GPU-less CI runners (#1593, addresses #1674 items 1+2).
Docs¶
- Filament materials documentation (#1919 Part C): every
.matsource now carries a header comment block (purpose, used-by node/loader, per-parameter contract, matc flag profile), and theCONTRIBUTING.md"Filament runtime ↔ .filamat ABI invariant" section is updated with thetools/GenerateFilamat.shworkflow, thequality-gate.shdrift gate, and the four A/B/C/D matc flag profiles.
v4.11.2 — 2026-05-21¶
Added¶
arsceneview: AR depth-of-field driven by ARCore environment depth — newarDepthOfField(view, camera, options)composable +ARDepthOfFieldOptions(focusDepth, blurStrength, enabled)data class wire Filament's native DoF post-pass to the same z-buffer thatARCameraStream's depth-occlusion material already writes (gl_FragDepthincamera_stream_depth.mat), so tapping a near object throws the far background out of focus and vice-versa — both the virtual scene and the camera background blur from the same focus point. No new.filamatrequired. Tap-to-focus helperFrame.depthFocusDistance(xPx, yPx): Float?reuses the depth hit-test added in #1712. Opt-in (off by default; zero cost on disabled frames). RequiresConfig.DepthMode.AUTOMATIC/RAW_DEPTH_ONLY+ARCameraStream.isDepthOcclusionEnabled = true. NewARDepthOfFieldDemoin the sample app demonstrates the canonical wiring;llms.txtdocuments the API surface (#1716).arsceneview: Scene Semantics API —Config.SemanticMode.ENABLEDis now support-gated viaARSession.configure(silently downgrades toDISABLEDon devices without the on-device ML model, matching the depthMode / flashMode auto-fallbacks), and three newFrameextensions expose the per-pixel labels:Frame.semanticImage(): Image?(R8 label ordinal raster),Frame.semanticConfidenceImage(): Image?(R8 confidence raster), andFrame.semanticLabelFraction(label: SemanticLabel): Float(cheap GPU-backed pixel-share query, returns 0f when semantics are off / not yet available). Comes with a newARSceneSemanticsDemoshowing a live top-3 label HUD over the camera feed. Outdoor only — the ML model has no indoor training data. The custom.filamatlabel-overlay material is tracked separately as a follow-up (matc toolchain ABI work) (#1730).arsceneview: surfaced ARCore CPU camera image access viaFrame.cameraImage(): Image?— a 1-line wrapper aroundacquireCameraImage()returningnullonNotYetAvailableExceptionand documenting the caller-owneduse { }lifecycle. Unblocks ML Kit / OpenCV / custom CV pipelines. Pair with the newcameraConfigFilter { facing = …; targetFps = …; depthSensor = …; stereoCamera = … }DSL onARSceneView.sessionCameraConfigto pick a session-wideCameraConfig(resolution, FPS, depth/stereo-sensor usage) without hand-rollingSession.getSupportedCameraConfigs(filter). Falls back to the session's current config when no match exists so session creation never crashes (#1733).- Jetpack XR foundation — runtime availability check + integration design (#1738). Adds
io.github.sceneview.ar.xr.XrFeatures.isAvailable(context)to gate Android XR (headsets, glasses) code paths, declares theandroidx.xr.arcore:arcore:1.0.0-alpha14dependency alias, and records the module / runtime decision inarsceneview/docs/JETPACK-XR-INTEGRATION.md. Hand tracking node + demo (Slice 2) and Jetpack XR face tracking node (Slice 3) ship in follow-up PRs. Phone-only apps are unaffected — the XR dependency is opt-in and the Perception runtime is reached via reflection. arsceneview:sealed class ARSessionFailure— typed taxonomy covering all 25 ARCore exception subclasses (install, permission, camera, quota, cloud-anchor, augmented-image, recording/playback, session/config) plus anOtherescape hatch. NewARSceneView(onSessionFailure: ((ARSessionFailure) -> Unit)? = null)callback dispatches alongside the legacy raw-ExceptiononSessionFailedso apps can do exhaustivewhenmatching (the compiler catches missing cases the day ARCore adds a new failure category). OriginalExceptionpreserved on.causefor every subtype.CloudAnchorNode.onHostedalready passed the specificCloudAnchorState(not a binaryisError),AugmentedImageNode.trackingMethod+onTrackingMethodChangedalready surfacedFULL_TRACKINGvsLAST_KNOWN_POSE, andConfig.addAugmentedImage'sImageInsufficientQualityExceptionis now routed via the newARSessionFailure.ImageInsufficientQualitysubtype. Backwards compatible — existingonSessionFailedcallers see no change (#1759).- arsceneview: new
SceneUnderstandingdata class +ARSceneView(sceneUnderstanding = ...)parameter that groups four scattered AR rendering flags (occlusion,lighting,physics,planeVisualization) into one discoverable knob — mirrors RealityKit'sARView.environment.sceneUnderstanding.optionsfor cross-platform parity. The parameter is opt-in (defaults tonull); when null, the individual flags retain their pre-#1767 defaults. Named constantsSceneUnderstanding.Full,.Minimal,.Nonecover the common configurations. AI assistants now find one parameter instead of four (#1767). arsceneview: rounded out the ARCore recording/playback surface (#1770).rememberARPlaybackStatus(session): State<PlaybackStatus>— Compose State that surfacesNONE/OK/FINISHED/IO_ERROR(theFINISHEDtransition is the only public end-of-replay signal, useful for rewind / loop / next-dataset logic).ARRecorder.State.IO_ERROR— distinct from genericERROR. Set byrecordFrame(session)when ARCore reportsRecordingStatus.IO_ERROR(disk full, storage detached, permission revoked mid-recording) so apps can offer a "clear cache and retry" CTA.ARRecorder.addTrack(uuid, mimeType)+ARRecorder.recordTrack(handle, frame, data)— exposes ARCore'sRecordingConfig.addTrack+Frame.recordTrackDataflow for ML annotation / ground-truth / custom sensor packets written inside the same MP4.ARSceneView(playbackDatasetUri: Uri? = null)— scoped-storage equivalent of theplaybackDataset: File?param (Android 10+). Acceptscontent://URIs straight from the SAF picker so apps don't have to copy into app-private storage. Mutually exclusive withplaybackDataset— setting both throwsIllegalArgumentException.samples/web-demo(QA): IWER (Immersive Web Emulation Runtime,iwer@^2.2.1) WebXR shim is now injected into the Playwright page viapage.addInitScript(...)under a Meta Quest 3 emulated device profile, and a newtests/webxr.spec.tsclicks#enter-ar/#enter-vrand asserts no console errors, no WebGL context loss, and no unhandled rejections — closing the WebXR scaffolding gap (#1878, follow-up of #1748). The rich replay test soft-skips until a real recorded XR session fixture is added (separate follow-up, requires a real WebXR-capable device).- QA harness: advisory web-perf scaffold (#1879). New
.claude/scripts/web-perf-qa.shruns Lighthouse (mobile preset) againstsamples/web-demoand emitsweb-perf-summary.jsonwith FCP / LCP / CLS + the Lighthouse performance score. Wired intodevice-qa.shas an advisory sub-leg of the web run (continue-on-error, never blocks the release gate). Thresholds are deliberately deferred — follow-up tracked. samples/android-demo: developer-only debug toggle that force-emits anyTrackingFailureReasonso the actionable-message overlay wired by #1735 can be validated indoors without staging a real failure (dark room, textureless surface,EXCESSIVE_MOTION, etc.). NewForcedTrackingFailuresingleton +ForceTrackingFailureMenu()composable section undersamples/android-demo/.../common/— visible only whileDemoSettings.qaModeis on (long-press the demo's peek-chip or launch with--ez qa_mode true), so end users never see it. Wired intoARImageDemoas a proof-of-concept; a follow-up issue covers the remaining 11 AR demos that share the sametrackingFailureMessageoverlay (#1881).samples/android-demo: extended the developer-only force-tracking-failure debug toggle (#1881 / #1887) to the remaining 11 AR demos that consumeTrackingFailureReason.ForceTrackingFailureMenu()is now reachable from each demo's Settings sheet (still gated byDemoSettings.qaMode, so end users never see it), and each demo's status-overlay path now readsForcedTrackingFailure.overridedirectly so flipping the override re-renders the banner without waiting for the next ARCore tracking-failure callback. Wired demos:ARCloudAnchorDemo,ARDepthOcclusionDemo,ARDepthVisualizationDemo,ARImageStabilizationDemo,ARInstantPlacementDemo,ARPlacementDemo,ARRawDepthPointCloudDemo,ARRecordPlaybackDemo,ARRerunDemo,ARSceneSemanticsDemo,ARStreetscapeDemo(#1888).
Changed¶
samples/web-demo(QA): Playwright bumped to^1.59.0and the legacyvideo: 'on'capture is replaced with apage.screencast-drivenscreencasttest fixture that brackets every test, writes one.webmper test undertest-results/screencasts/<slugified-title>.webm, and exposes ascreencast.chapter(title, description?)API for tagging meaningful boundaries (tab switch, model load, failure).device-qa.shmirrors the recordings into$ARTIFACTS/web-screencasts/so the web leg now ships per-test video parity with the Maestro Android / iOS legs (#1748)..claude/hooks/pre-risky-github-op.sh: added anSV_BATCH_REBASE=1env-var escape hatch so legitimate multi-PR rebase batches (e.g. a 9-branch ARCore audit sprint) no longer require clearing~/.claude/logs/force-push.logto bypass the 1-force-push-per-24h cap. The bypass logs an explicit notice to stderr and tags the entry[SV_BATCH_REBASE]for auditability, and the BLOCK message now points to the escape hatch instead of suggesting log tampering (#1796).- Append-only demo registry for
samples/android-demo(#1797). Adding a demo to the Android sample app no longer requires editing shared files. Each demo is registered by a single*Fragment.ktfile underio.github.sceneview.demo.fragments; a collator (samples/android-demo/scripts/collate-demos.sh) aggregates them intoGeneratedDemos.kt, sorted by id so two parallel PRs never collide on the same anchor. The quality gate runs the collator in--checkmode to block stale generated files. - ci:
.claude/scripts/worktree-auto-prune.shpolish pass — respectsgit worktree lockby default (with--unlock-lockedoverride, #1833), broadens active-session detection fromnode/claudeto every process whose cwd is inside a worktree so gradle daemons / Python venvs / IDE indexers also block prune (#1834), writes one JSON line per evaluated worktree to~/.claude/logs/worktree-prune-YYYYMMDD.logfor post-incident forensics, batches the merged-PR lookup into a singlegh pr list(was N ×gh pr view), and wrapslsofintimeout 10sso a hung scan can't hang the prune (#1839). New.claude/scripts/test-worktree-auto-prune.shexercises 7 scenarios — merged, unmerged, dirty, locked, locked+--unlock-locked, live subprocess,--keep— and runs advisorily insidequality-gate.sh(#1835). CONTRIBUTING.md now documents the full skip ladder and flag set. - AR perf minor polish (#1846). Tier-2 Wave-4 PERFORMANCE follow-ups. None individually MAJOR; collectively close out the audit's remaining minor findings.
DepthMeshCollisionTestcolumn-order test hardened — added a 90° Y-rotation + non-axis-aligned translate case that compares the inline matmul (post-#1810) against kotlin-math's referenceMat4 * Float4. A column-swap in the inline math would now fail on every non-zero rotation component instead of silently passing the translate-only fixture.DepthMeshNode.acquireDirectBuffercapped at a newMAX_UPLOAD_BUFFER_BYTES = 1 MBceiling — a one-off oversized depth image no longer permanently inflates the upload-buffer cache. Buffers under the cap retain the no-shrink amortisation behaviour from #1810. NewDepthMeshNodeUploadBufferCapTestpins the invariant.DepthHitResultNodeper-framePose.makeTranslationdocumented as load-bearing — investigation found ARCore'sPoseis immutable by design andDepthHitResultcarries no reusable Pose, so one alloc per node per frame is the floor for this surface. Inline KDoc steers future perf passes away from a false "fix".rememberARPlaybackStatusalready migrated to baretry/catch (e: RuntimeException)in #1857 — noThrowablewrapper allocation on IO_ERROR frames.samples/android-demo: per-demostrings.xmlfragments. Title, subtitle, and demo-specific UI strings now live in dedicatedres/values/strings_demo_<id>.xmlfiles alongside each demo's*Fragment.kt, so two parallel PRs adding two different demos no longer collide on the centralstrings.xml. Android's resource merger fans everyres/values/*.xmlin at build time, soR.string.demo_*references resolve identically (no Kotlin / composable changes). The sharedstrings.xmlkeeps only app-level strings (navigation, AR launcher, About, accessibility…). Follow-up of #1797's append-only fragment registry (#1870).samples/android-demo:collate-demos.shnow also rewrites the "Sample app demos (Android)" section ofllms.txt(and its mirrordocs/docs/llms.txt) between dedicated marker comments, sourced from the same per-demo*Fragment.ktfiles that driveGeneratedDemos.kt. Adding a new demo no longer touches anyllms.txt: drop the fragment, run the collator, regenerate the MCP bundle (node mcp/scripts/generate-llms-txt.js), commit.--checkmode bit-compares all three outputs and the existingcheck-llms-drift.sh+quality-gate.shwiring picks the new section up unchanged (#1871, follow-up of #1797 / PR #1869).
Fixed¶
.claude/scripts/impact-check.sh: trace line per check +--failflag + ERR trap so the script no longer exits 1 silently in lean / sparse clones. Each check now announces itself on stderr (the last trace line points at any unexpected failure), every path-dependent check[SKIP]s instead of dying when its inputs are absent (sceneview/,arsceneview/,SceneViewSwift/),grep | wc -lpatterns are guarded against thepipefailzero-match exit, and an ERR trap names the dying check + line. Default exit is now 0 (report-only);--failopts in to non-zero for the quality gate.SV_IMPACT_TRACE=1forcesset -x; auto-trace fires when stdout is not a TTY (CI / agent) unlessSV_IMPACT_TRACE_AUTO=0(#1782, #1786).build.gradle: document that thewebpack <5.107.0Yarn resolution pin (added in #1791 for:sceneview-web:jsBrowserProductionWebpack) also covers:sceneview-web:jsTestand:sceneview-web:jsBrowserDistribution. The root cause is shared —kotlin-web-helpers/dist/tc-log-error-webpack.jsstill doesrequire("webpack/lib/ModuleNotFoundError")after webpack 5.107.0 moved that file tolib/errors/, and karma surfaces the resolution failure with a misleading top-of-stackkarma/bin/karmaline. The pin already keeps fresh-clone:jsTestruns green; this commit just makes the comment match the actual scope so future bound-lifts don't accidentally re-break test execution. Validation on a clean clone (rm -rf build/ && ./gradlew :sceneview-web:jsTestand:jsBrowserDistribution): both BUILD SUCCESSFUL with webpack 5.106.2 resolved (#1785).- arsceneview:
DepthMeshNode.computeAabbnow clamps every half-extent belowDEGENERATE_AABB_HALF_EXTENT_Mto the degenerate cube, not just the all-empty-positions case (#1806). The earlier #1783 fix only handledpositions.isEmpty(); geometry where every sample shared the same coordinate (e.g. a constant-range depth image, or the first frame after a scene reset where only one off-grid(0,0,0)vertex made it through) still emitted a zero half-extent and tripped Filament'sAABB can't be emptySIGABRT. AddedDepthMeshNodeAabbTestwith the issue's reference case (all positions(0.5, 1.0, -2.0)) plus mixed-axis and single-vertex cases. Surfaced by the May 2026 Tier-2 SECURITY audit. - arsceneview: defensive input validation on the AR depth pipeline (#1812).
DepthMeshNode.updatenowrequires non-zero camera intrinsics (width, height, focal length) so degenerate ARCore frames raise a clear error instead of poisoninglatestSnapshotwithInf.Frame.hitTestDepthandunprojectDepthPixellikewise reject zero focal length — the latter throwsIllegalArgumentExceptionfor direct callers; the former returnsnull.DepthCollider.setBodiesRegionrejects flat-packed arrays whose size is not a multiple of 3 (IllegalArgumentException) and silently falls back to disabled culling when the resulting region is non-finite.nearestSurfaceYBelowskips out-of-bounds index triplets so a future drift betweenpositions/indicescannot AIOOBE on the render thread. All paths covered by JVM unit tests. - ci:
app-store.yml's submit step now GETs the version record'sappStoreVersionSubmissionrelationship and DELETEs any stale submission before re-POSTing — so the submission CREATE is idempotent across re-runs. When the #1687 + #1795 absorption logic retargets a stranded draft, that draft's old submission used to remain attached and 403 every subsequent CREATE ("Allowed operation is: DELETE"). v4.11.1 hit this on the stranded367draft → renamed to4.11.1→ POST refused. Closes the last loose end of the #1795 / #1687 saga (#1831). DepthMeshNodeno longer leaks the oldVertexBuffer/IndexBufferwhen an upload step throws mid-frame (engine teardown is the realistic trigger).rebuildBuffersIfNeededreturns the freshly-built buffers without mutating theowned*fields;uploadGeometrycommits the swap + destroys the old buffers ONLY aftersetGeometryAtreturns. On any exception the new buffers aresafeDestroy-rolled-back and the owned* fields remain reachable fordestroy(). Closes a latent leak introduced by the #1805 UAF fix (#1840).ARDepthColliderDemo: collapsed the per-ballapply.onFramefan-out into a single Scene-levelonSessionUpdatedcallback —publishCollisionRegionnow runs ONCE per AR frame (was N times for N balls → ~300 transientFloatArray/sec at 5 balls × 60 fps). Replaced themutableListOf + activeBallNodes += thispattern with a per-ball-countarrayOfNulls<SphereNode>(ballCount)slot store written by index. Recompositions that don't changeballCount(slider, theme, parent state) no longer leak stale node refs, so the region-cull AABB stays bounded. Region-cull payload is now packed by a purepackCentres(...)helper with a JVM regression test pinning the "no stale entries bleed through" invariant. Closes #1842.arsceneview: hardenARRecorder+cameraConfigFilter+ playback wiring against five privacy / misuse / threading regressions surfaced by Tier-2 Wave 4 security review (#1845):ARSceneView(playbackDatasetUri = …)now allowlistscontent://andfile://schemes only (rejectshttps://,data:, custom schemes at the SceneView boundary instead of handing them silently to ARCore — caller-side permission requirements documented on the KDoc);cameraConfigFilter { targetFps = emptySet() }raisesIllegalArgumentExceptionat builder time (was silently degrading to the session default camera config, which on Augmented Faces sessions is front-facing — a developer requesting back-only got the front camera with no signal) and the runtime catch is narrowed to ARCore's documentedRuntimeExceptionfailure point (Session.getSupportedCameraConfigs) so builder errors propagate to dev-time tests;ARRecorder.recordFrameIO_ERROR transition now commitsstate+errorMessageinside aSnapshot.withMutableSnapshot { }so Compose readers can no longer observe the in-betweenstate == RECORDINGpaired with a non-nullerrorMessage;ARRecorder.recordTrack(handle, …)short-circuits tofalsewhenhandlewas not registered viaaddTrackon the same recorder (was forwarding to ARCore — cross-recorder reuse leaked packets between unrelated recordings);ARRecorder.addTrackis bounded toMAX_PENDING_TRACKS = 64and a newclearTracks()API drops the in-memory registry (prevents theaddTrack(UUID.randomUUID(), …)leak when wired into a recomposing block — unique UUIDs bypass the idempotent dedup, the cap surfaces the misuse at the call site).- CI:
quality-gate.shnow blocks llms.txt mirror drift (#1847). The drift detectors fordocs/docs/llms.txtand the MCP bundlemcp/src/generated/llms-txt.tsused to live only insync-versions.sh, which is not called by the PR-blocking gate. A new dedicatedcheck-llms-drift.shis wired intoquality-gate.shso any divergence from rootllms.txt(e.g. theDepthHitResultNodedrift that landed via #1822) fails the gate instead of silently sitting onmain. - docs, scripts: plug version-bump tooling holes for derived doc surfaces (#1848). Bumped stale Maven coordinates / SPM tags / CDN @version pins in
arsceneview/Module.md,sceneview/Module.md,docs/docs/manifest.json,docs/docs/structured-data.json, and the three agent skills underagents/sceneview*/(SKILL + references/cheatsheet + references/migration + references/recipes) — they had drifted to 4.3.x / 4.4.x / 4.9.x. Added 14 ERROR-level checks to.claude/scripts/sync-versions.shcovering manifest.jsonrelated_applications[].id, structured-data.jsonsoftwareVersion+releaseNotestag + Maven prose, plus every per-skill Maven coordinate / npmsceneview-web@/@sceneview-sdk/react-native@/ SPM tag prose line, with matching--fixrewrites so future releases catch the drift. samples/android-demo:ARRawDepthPointCloudDemonow guides the user through motion-stereo convergence on non-LiDAR Pixels — a first-launch overlay ("Move your device for raw depth to converge") that auto-dismisses on the first non-zero frame or after 8 s, plus a passive top-right chip when the point count stays at zero for more than 2 s. The default confidence threshold is also lowered from 63/255 to 32/255 so motion-stereo's first frames produce visible points immediately. Before, a fresh launch showed "0 points" with no indication that the demo needs phone motion, which read as a broken demo (#1873).samples/android-demo:ARDepthColliderDemonow spawns balls in front of the current camera pose instead of the AR-session origin, so the balls are always visible regardless of how the user has moved before tapping Drop (#1874), and hides the underlyingDepthMeshNoderenderable by default — the cream dotted grid that the default material drew on every real surface was distracting and ambiguous. A new "Show depth mesh (dev)" Settings switch re-enables the visualization for collider debugging (#1875).samples/android-demo:ARPlacementDemonow surfaces a screen-centre placement reticle so the user can see where their next tap will land — a thin unlit cyan disc that follows the centre-of-screen hit-test result each frame via the AR-scopeHitResultNode, with an "Aim at a surface…" prompt when no hit is detected (#1882). The previously-empty Settings sheet is populated with a bundled-model chip row (Damaged Helmet / Fox / Lantern / Toy Car / Shiba — or "Auto-cycle"), a "Snap to plane" toggle (default ON, gating tap acceptance to detected planes), a "Show reticle (dev)" toggle (default ON), and a prominent filled "Clear All" button — the empty placeholder above the Reset row that Pixel 9 QA flagged is gone (#1883).HitResultNodedefaults to plane-only (#1891). The screen-coordinateHitResultNode(xPx, yPx, ...)overload now defaultspoint = false,depthPoint = false,instantPlacementPoint = false, plus a new defensiveminCameraDistance: Float? = 0.3ffloor that drops hits closer than 30 cm from the camera. Pixel 9 device-QA surfaced the previous wide-open defaults causing a fullscreen overlay on session start — depth / feature hits before motion-stereo convergence return positions <10 cm from the lens, and a child placement disc then blanks the camera feed. Opt each filter back in explicitly once your scene is tracking-stable.samples/android-demo+samples/ios-demo: restored the Sketchfab integration on the published Play Store and TestFlight binaries. TheSKETCHFAB_API_KEYGitHub secret was empty (or whitespace) for several recent releases, soBuildConfig.SKETCHFAB_API_KEY/Info.plist:SketchfabAPIKeyresolved to""andSketchfabConfig.apiKeyreturnednull. That silently hid the three Explore-tab carousels (Staff Picks / Most Liked / Recently Added), turned the search bar into a no-op (queries got persisted to Recent Searches but were never executed), and forced everySketchfabAssetResolver-driven streamed demo (MultiModelDemo, ARPlacementDemo, ARInstantPlacementDemo, scene gallery, ...) onto its bundled-GLB fallback. The secret has been re-issued with a verified-valid token; #1910 tracks moving the request path throughmcp-gatewayso this regression class can't recur (#1909).- Strip lying "implemented" badges from the Flutter demo (#909).
samples/flutter-demo/lib/pages/features_page.dartwas claiming green for several methods whose iOS bridge path is a no-op (ModelNodepos/rot,onTap,onPlaneDetected,Environment). Those cards are now labelled "Android only" with the iOS gap pointed at the #909 umbrella. Added a FlutterMethodChannelsmoke-test suite and a React NativeARRecorderJest smoke-test scaffold so future drift surfaces as a red test instead of a green badge. sceneview-webSCENEVIEW_VERSIONconstant lagged 2 releases (4.9.0while shipping4.11.1). Bumped to4.11.1and promoted thesync-versions.shcheck for this code-resident constant from WARN-only to a hard MISMATCH so it can never silently drift again. The regression-pin jsTest (#1357) was bumped in lockstep.- Contributor scripts:
worktree-auto-prune.shno longer silently deletes worktrees with unmerged work whengit fetchfails. Previously a failed fetch only printed a warning and continued with whatever localorigin/mainwas cached, so a worktree on a branch with commits past main could be misclassified asahead=0and removed. The fetch now exits with an error; pass--allow-staleto opt back into local refs for offline runs. In--allow-stalemode, candidates additionally require a merged-PR signal —ahead=0alone is no longer trusted. New active-session guard (on by default,--no-check-active-sessionsto disable): a worktree is skipped if any livenode/claudeprocess has its cwd inside it. The scan re-runs immediately before the destructive loop to close the prompt-window race. The wrappercleanup-branches-worktrees.shpropagates--allow-stalewhen its own fetch fails so offline runs through the wrapper still work.
Docs¶
arsceneview: TightenARDepthOfFieldKDoc with the upstream Filament verification (colorPassOutput.depthis the buffergl_FragDepthwrites to, so DoF post-pass + camera-stream depth occlusion compose without surprises) and surface three device-QA caveats that need eyeballing on real hardware: reverse-Z + early-Z culling around theclip.z = 0.9999fvertex hack, MSAA resolve filtering on the depth attachment, andcocParamscalibration against the AR camera node's projection. Pure docs change; no API/behaviour delta (follow-up to #1716).README.md,llms.txt, and the docs landing page now explicitly position SceneView as the Compose-native successor to Google's archived Sceneform — ARCore for perception, Filament for rendering, Jetpack Compose for the API — so developers and AI assistants searching for a Sceneform replacement find SceneView (#1736).- docs: cross-platform parity table in
cheatsheet-ios.mdmapping the four May 2026 Android-only AR surfaces (DepthMeshNode/DepthCollider/Frame.hitTestDepth/CloudAnchorNode.hostFuture-cancel) to their RealityKit / ARKit counterparts; rootllms.txtcross-platform notes added in each section pointing readers to the cheatsheet. SceneViewSwift implementation work split into #1859 (CloudAnchorNode Future) + #1860 (Scene Reconstruction). #1813 arsceneview: rewrote theARSessionFailureKDoc +llms.txtexamples to use a fully exhaustivewhen(all 25 subtypes +Other) and removed theelse -> showGenericRetryCta()fallback that silently defeated the sealed-class compile-time-safety contract introduced by #1759. Also added a "compact" pattern showing how to dispatch many subtypes via a category-mapping helper withoutelse ->. AI agents copy-pasting the snippet now keep the exhaustive-whenguarantee (#1843).
v4.11.1 — 2026-05-20¶
Added¶
rememberDepthCollider() — depth-driven static physics collider so PhysicsNode bodies bounce off the real floor / table / wall in AR. Thin wrapper over DepthMeshNode (#1739): each rebuild's vertex/index buffers feed a per-frame surface lookup via the new FloorProvider interface on PhysicsBody. SceneView port of arcore-depth-lab's "Collider" scene (#1713).
- Android demo: added an ar-depth-visualization AR demo that renders the ARCore environment depth image as a false-color overlay (warm = near, cool = far), with a slider that blends the live camera feed (0) and the colorized depth map (1). The colorization runs through pure-Kotlin helpers in samples/android-demo/.../demos/internal/DepthVisualization.kt covered by JVM unit tests, and the demo handles "depth not supported" and "depth warming up" with explicit banners — never a black screen (#1714).
- Android demo: added an ar-raw-depth-point-cloud AR demo that visualizes ARCore's Config.DepthMode.RAW_DEPTH_ONLY output as a screen-space point cloud. The demo acquires raw depth + the companion confidence image on every frame, drops samples below a Compose-slider-driven confidence threshold, false-colors the survivors with a warm-near / cool-far ramp, and renders them over the camera feed via a Canvas. The filtering/sub-sampling logic is extracted as pure-Kotlin internal helpers in samples/android-demo/.../demos/internal/RawDepthCloud.kt and covered by 14 JVM unit tests. Honest unsupported / warming-up states surface explicit banners — never a black screen (#1715).
- arsceneview: surfaced ARCore v1.45+ Flash Mode as a new flashMode: Config.FlashMode parameter on ARSceneView (default OFF). Toggling between OFF / TORCH recomposes the session config reactively, and unsupported devices / front-camera sessions silently downgrade to OFF via Session.isFlashModeSupported() — matching the existing depthMode auto-fallback behaviour (#1732).
- arsceneview: surfaced CloudAnchorNode.TTL_DAYS_RANGE = 1..365 and added the CloudAnchorRegistry interface plus a SharedPreferencesCloudAnchorRegistry default for persisting hosted Cloud Anchor IDs (name → cloudAnchorId, hostedAt, ttlDays) across app launches, with isExpired() / purgeExpired() helpers. CloudAnchorNode.host() now validates ttlDays ∈ 1..365 and documents the required ARCore data-privacy disclosure (#1734).
- DepthMeshNode — reify ARCore environment depth as a renderable Filament mesh. New rememberDepthMesh() + DepthMeshNode composables in ARSceneScope turn the live depth image into a triangulated grid in the scene, with edge-discontinuity culling so triangles never stretch across depth jumps. Rebuild is interval-rate-limited (default 5 Hz). Exposes the camera-space vertex / index buffers via a DepthMeshSnapshot callback so downstream consumers (depth-driven physics collider, debug overlays) can read the geometry without poking Filament internals. SceneView equivalent of arcore-depth-lab's ScreenSpaceDepthMesh. (#1739)
- Async Future cancellation across CloudAnchor / Terrain / Rooftop (#1768). CloudAnchorNode.host now returns the underlying HostCloudAnchorFuture so callers can cancel pending Google Cloud requests on UI disposal (avoiding billing accrual for users who navigated away). CloudAnchorNode.resolve, TerrainAnchorNode.resolve, and RooftopAnchorNode.resolve carry explicit return types (ResolveCloudAnchorFuture / ResolveAnchorOnTerrainFuture? / ResolveAnchorOnRooftopFuture?) for the same reason. Billing rationale + DisposableEffect.onDispose { future.cancel() } pattern documented in KDoc and llms.txt. JVM unit tests pin the return-type contract via reflection.
- arsceneview: surfaced four Geospatial accessors as Compose-friendly helpers (#1769). rememberCameraGeospatialPose(session) returns a State<GeospatialPose?> that updates each frame with the live device lat/lng/altitude (null until ARCore acquires a GPS lock + Earth.trackingState == TRACKING). GeospatialPose.snapshot() captures all 7 fields (lat/lng/altitude/heading/horizontalAccuracy/verticalAccuracy/orientationYawAccuracy) into a GeospatialPoseSnapshot data class so apps can retain them across frames — the existing .transform extension drops the four accuracy / heading fields. rememberEarthState(session) exposes Earth.EarthState (ENABLED / ERROR_INTERNAL / ERROR_NOT_AUTHORIZED / ERROR_RESOURCE_EXHAUSTED / ERROR_APK_VERSION_TOO_OLD / ERROR_GEOSPATIAL_MODE_DISABLED) as Compose State. Session.awaitVpsAvailability(lat, lng) is a suspend wrapper around checkVpsAvailabilityAsync — apps can gate "place Terrain anchor" buttons on actual VPS coverage instead of guessing and surfacing ResourceExhaustedException after a network round-trip.
- ARCore extension one-liners (#1771). Thin Kotlin wrappers around frequently-needed ARCore APIs: HitResult.distance, Camera.displayOrientedPose, Plane.polygon, Plane.subsumedBy, Camera.intrinsics(useTexture) returning a CameraIntrinsicsSnapshot (focalLength / principalPoint / imageWidth / imageHeight), public Frame.depthImage() / Frame.rawDepthImage() / Frame.rawDepthConfidenceImage() accessors that swallow NotYetAvailableException, and suspend ArCoreApk.awaitAvailability(context). Documented in llms.txt under "Low-level helpers".
- arsceneview: added types: Set<StreetscapeGeometry.Type> and minQuality: StreetscapeGeometry.Quality filter parameters to ARSceneScope.StreetscapeGeometryNode (default {BUILDING, TERRAIN} / Quality.NONE — no filtering). Apps can now request only BUILDING meshes (drops the noisy ground terrain in dense urban scenes) and gate on BUILDING_LOD_2 to render only the higher-LOD geometry — saves a frame-rate cliff on low-end devices. Geometries that don't pass the filter become composable no-ops and never allocate Filament buffers. The companion cameraConfigFilter { … } DSL covering this issue's second acceptance criterion (DepthSensorUsage / StereoCameraUsage knobs on the camera config filter) ships in #1733 (#1772).
- arsceneview: new ARSceneScope.DepthHitResultNode(xPx, yPx, content) composable — Compose-idiomatic mirror of HitResultNode for placement against the ARCore depth image. Each frame re-runs Frame.hitTestDepth and moves to the resulting world-space surface point; depthHitResult exposes the live DepthHitResult for surface-normal-aligned content (#1814).
Changed¶
- AR demos now surface ARCore tracking-failure reasons via a single
trackingFailureMessage(reason)helper ported from arcore-android-sdk'sTrackingStateHelper. EachTrackingFailureReason(BAD_STATE,INSUFFICIENT_LIGHT,EXCESSIVE_MOTION,INSUFFICIENT_FEATURES,CAMERA_UNAVAILABLE) maps to a localised string resource (tracking_failure_*instrings.xml), giving the user actionable guidance instead of nothing. Inlinedwhenbranches across 8 AR demos (Image, DepthOcclusion, ImageStabilization, InstantPlacement, Placement, Rerun, RecordPlayback, Streetscape) collapse into a 1-line helper call. Addresses the "no guidance" half of #1615 (#1735). - arsceneview: threading-hardening sweep on the AR depth and cloud-anchor paths (#1811).
DepthMeshNode.update / latestSnapshot / currentVertexBuffer / currentIndexBuffer / onMeshRebuiltnow carry@MainThreadannotations with KDoc notes spelling out "render-thread only — reading from a background coroutine is unsupported".CloudAnchorNode.hostTaskis@VolatileandcancelHost()wraps its read-cancel-clear sequence insynchronized(this)so callers can safely cancel an in-flight host fromviewModelScope.launch { … }without racing the ARCore async callback (which fires on the GL/render thread).DepthCollider.setBodiesRegiongains a KDoc render-thread pin matching the surroundingfloorYAt/ingestSnapshotcontract. A newCloudAnchorNodeThreadingTestreflects the@Volatileand exercises the synchronized contract under contention.
Fixed¶
- arsceneview: per-frame
IndirectLightno longer leaks native memory across long AR sessions with intermittent light estimation. TheIndirectLightbuilt inonARFrameis now tracked via a dedicatedAtomicReferenceand destroyed explicitly on every supersession or onDisposableEffectteardown, independent ofscene.indirectLightmutations by third parties. The rebuild decision (estimation vs. environment baseline per channel) is extracted to a purepickIndirectLightSourceshelper covered by JVM unit tests (#1756). - arsceneview: clarify the depth
ByteBufferlifecycle invariant inARCameraStream— the buffer borrowed from ARCore's depthImageis now documented as intentionally NOT cloned, with the upload-completed callback as the load-bearing synchronisation point that closes the ARCore image exactly once. Updates the previous misleading comment that claimed the buffer was cloned. Adds a pure-JVM sentinel test pinning thebuffer.clear()metadata-only contract and the setter-doesn't-allocate invariant (#1757). - Cache the identity tangent buffer on
AugmentedFaceNode(#1758). The(0, 0, 0, 1)identity-quaternion buffer used to honour Filament's FLOAT4 TANGENTS stride contract under unlit face materials is now built once at mesh creation and reused unchanged — no per-vertex rewrite on subsequent calls, even when the cached size matches. JVM unit test pins the cache hit/miss contract. - react-native:
@sceneview-sdk/react-nativepackage.jsonnow declarespublishConfig.access=public, anchoring the scoped-package public-access intent inline.release.yml'snpm publish --access publicCLI flag stays as belt-and-suspenders so any future workflow_dispatch retry or manual republish from a fresh checkout cannot regress to a 404 on the registryPUT(#1788). - build: v4.11.1 re-validates the
sceneview-webKotlin/JS production webpack chain that broke on v4.11.0 — webpack 5.107.0 movedlib/ModuleNotFoundError.jstolib/errors/ModuleNotFoundError.jswhile kotlin-web-helpers still resolved the legacy path, crashing every:sceneview-web:jsBrowserProductionWebpackinvocation. Fixed onmainby the webpack<5.107.0resolution pin in #1791; this release ensures the production publish +Deploy website + docspipelines run end-to-end on the v4.11.1 tag (#1789). - ci:
app-store.yml's submit step now readsVERSION_NAMEfrom the rootgradle.propertiesas itsworkflow_dispatchfallback forASC_VERSION_STRING, instead ofbuild_version(which returnsCFBundleVersion— the build number, not the marketing version). v4.11.0's manual deploy created a nonsensical App Store version record named367and 403'd on submission because of this; the fallback is now anchored to the project's single source of truth (#1795). - arsceneview: closed a use-after-free window in
DepthMeshNode.rebuildBuffersIfNeeded(#1805). The oldVertexBuffer/IndexBufferweresafeDestroy'ed beforeRenderableManager.setGeometryAtrebound the renderable to the new buffers — for one frame the renderable referenced freed Filament native handles. Reordered to build new → rebind → destroy old so the renderable never points at freed memory. AddedDepthMeshNodeBufferRebuildTestJUnit suite that pins the ordering across two successive growths via a mock-engine recorder. Surfaced by the May 2026 Tier-2 SECURITY audit. - arsceneview:
DepthColliderclass-level KDoc example now passes the collider throughfloorProvider = colliderinstead of the non-existentdepthCollider = colliderparameter (#1807). Code pasted from the KDoc previously did not compile. TheARSceneScope.rememberDepthColliderKDoc and thePhysicsNodeKDoc already used the correct form, so the bug was isolated toDepthCollider.kt. - sceneview: deprecated mass-overload
PhysicsNode's@Deprecated(ReplaceWith(...))now preserves the newly-addedfloorProviderparameter (#1807). The IDE quick-fix on the deprecation previously silently stripped AR floor wiring. The deprecated overload itself also gained afloorProviderparameter so the replacement is a 1:1 source-compatible swap. mcp: regeneratedsrc/generated/llms-txt.tsso the npm bundle and the Cloudflare Worker gateway both ship the full May 2026 AR sprint surface (DepthMeshNode/rememberDepthMesh/rememberDepthCollider/Frame.hitTestDepth/HostCloudAnchorFuture/ResolveCloudAnchorFuture/ Future-returningCloudAnchorNode.host&resolve/TerrainAnchorNode.resolve/RooftopAnchorNode.resolve). Added async-versions.shCI drift guard that rebuilds the bundle in-memory and fails when it disagrees with rootllms.txt, so a future sprint can no longer land API additions inllms.txtwhile leaving MCP clients on a stale snapshot. Documented the regen step in the newmcp/CONTRIBUTING.md(#1808).docs: fixed brokenTerrainAnchorNode.resolveandRooftopAnchorNode.resolveexamples inllms.txt— both usedearth = earth, but the real signature takessession: Session(the function readssession.earthinternally). Code pasted from the docs now compiles. Healed mirror drift between rootllms.txtanddocs/docs/llms.txt(theCloudAnchorRegistry+ttlDaysblock from #1734 was missing in the mirror). Added aDisposableEffect.onDispose { future?.cancel() }snippet to the Terrain and Rooftop sections so AI agents emit the same cancel-on-dispose pattern they already produce forCloudAnchorNode(#1768). Added a "Threading" note toFrame.hitTestDepth(~L846) andDepthMeshNode(~L1039) — both must run on the AR frame / GL-main thread; KDoc said so already butllms.txtdidn't. Added "See also" cross-references betweenDepthMeshNode,rememberDepthColliderandFrame.hitTestDepthso devs landing on one discover the other two (#1809).- arsceneview, sceneview: kill per-frame allocation hot paths in the AR render loop (#1810).
ARScene.onARFrame: single-passfor (n in childNodes) when (n) { is PoseNode -> ...; is DepthMeshNode -> ... }replaces twofilterIsInstance<...>().forEach { }walks (~240 list allocations/sec at 60 fps on the render thread).DepthMeshNode.uploadGeometry: vertex / index upload now reuses two cached directByteBuffers grown in powers of two (was ~100 KB/s direct-buffer churn at 5 Hz, ~600 KB/s at 30 Hz).DepthMeshCollision.transformPositionsToWorld: inline 4×4 × (x,y,z,1) matrix multiply writes straight into the outputFloatArray, removing ~9k transientMat4 * Float3allocs/sec.PhysicsBody.step: velocity + position integrated as plainFloattriples, committed in exactly 2Positionallocs per body per frame (was 3-4 → ~1200/sec at 5 balls × 60 fps).ARDepthColliderDemo: now drivesDepthCollider.setBodiesRegion(...)once per frame from the active sphere centres + 15 cm padding so the KDoc-documented region-cull fast path is no longer bypassed (was ~540k tri-tests/sec; region-cull collapses to the bodies' shared AABB).- arsceneview: defensive
onDisposeordering onARScene's per-frameIndirectLightrebuild — clearscene.indirectLight = nullBEFOREengine.safeDestroyIndirectLight(...)so a lateonARFramequeued on the GL thread cannot dereference a freed native handle (#1814).
Docs¶
arsceneview: document ARCore 1.54's Geospatial Depth inllms.txtand onStreetscapeGeometryNode. EnablingConfig.DepthMode.AUTOMATICtogether withConfig.GeospatialMode.ENABLEDandConfig.StreetscapeGeometryMode.ENABLEDautomatically extends environment-depth accuracy from ~8 m (motion-stereo only) to ~65 m by fusing depth with Streetscape geometry + sensors. Every existing depth consumer (Frame.hitTestDepth,DepthMeshNode,rememberDepthCollider,ARCameraStreamocclusion) benefits transparently — no API change required (#1731).- docs: new migration block in
docs/docs/migration.mdfor theCloudAnchorNode.host()return-type change (Unit→HostCloudAnchorFuture, #1768). Covers the source-compatibility break + theDisposableEffect.onDispose { future.cancel() }recommendation with billing rationale (#1814). - llms.txt:
DepthHitResultNodesection andFrame.hitTestDepth@returnKDoc clarification documenting the single-vs-list asymmetry vsFrame.hitTest(depth at one pixel is unique) (#1814).
v4.11.0 — 2026-05-20¶
Added¶
- Android demo: added a
cameraDistancezoom deep-link parameter — a--ef camera_distance <f>intent extra and asceneview://demo/<id>?cameraDistance=<f>query parameter that override the 3D hero-orbit camera distance. This lets the Maestro device-QA flows exercise 3D camera zoom, which Maestro cannot do by pinch;.maestro/android/flows/demo.yamlnow captures a near + far framing formodel-viewer. Invalid or out-of-range values fall back to the demo's default framing (#1571). Frame.hitTestDepth(xPx, yPx)raycasts the ARCore depth image and returns aDepthHitResult(world position, camera-facing surface normal, distance) — placement onto any real-world surface, not just detected planes, inspired by arcore-depth-lab's "Oriented Reticle" (#1712).- New cinematic turntable camera:
applyCinematicOrbit(cameraNode, timeSeconds)drives a slow, eased "hero shot" orbit around the content — long lens, gentle downward tilt and a soft vertical bob. Three feel presets are provided (CinematicCameraProfile.HeroProduct,SlowCinematic,NeutralWeb);CinematicCameraProfile.Defaultis the contemplativeSlowCinematicprofile. Pairs withSceneView(autoCenterContent = true, cameraManipulator = null)for a one-call cinematic showcase. - Remote files loaded over http(s) — glTF/GLB models, KTX environments, textures — are now cached on disk by the new
FileCache. The first load downloads and persists the bytes; every later load reuses the cached file, so there are no repeated downloads and assets stay available offline. Caching is wired transparently intoFileLoader.loadFileBuffer, withContext.fileCacheDir/Context.clearFileCache()to inspect or reclaim it, andFileCache.enabledto opt out.
Changed¶
validate-demo-assets.shnow cross-checks every asset physically bundled under the demo asset roots againstassets/catalog.jsonand fails CI if a bundled asset is undeclared, making catalog drift a build failure instead of a manual discovery (#1666).- CI workflow hygiene: a detekt static-analysis step is wired into the
lintjob (advisory for now — detekt 1.23.8 registers no tasks on the current Kotlin 2.3 toolchain, so the step reports without gating PRs pending a detekt upgrade and baseline),docs.ymlartifact action versions are aligned, thequality-gatejob restores Gradle wrapper validation, and a stale Node-version comment intelemetry-ci.ymlis corrected (#1699, #1702, #1703, #1708).
Fixed¶
- Samples cleanup: dropped the stale "Coming in v1.1" version label from the iOS demo's coming-soon placeholders (now a plain "Coming soon" badge), and documented that AR demos intentionally skip the 3D first-frame loading scrim (#1361).
- iOS:
FogNode.heightBased(...)andFogNode.heightFalloffare now formally deprecated with a compile-warning instead of silently no-op'ing at runtime — RealityKit has no per-pixel height fog equivalent to Filament'sView.fogOptions.heightFalloff. UseFogNode.exponential(density:color:)instead. The iOS Fog demo no longer advertises a "Height" mode. (#1380) - Device QA runs no longer show
cancelledwhen only the advisory android/ar emulator leg is flaky (#1643). The emulator-legscript:blocks boundedadb wait-for-deviceanddevice-qa.shwith internaltimeouts. A flaky CI emulator now produces a clean step failure (absorbed bycontinue-on-error) instead of letting the job run totimeout-minutes— a timed-out job endscancelled, and a cancelled job drags the whole run conclusion red even when web/build/the other legs passed. - Release device-QA gate is now deterministic and non-blocking — it dispatches its own uncancellable Device QA run, waits with a hard timeout, treats web+ar as required and android as advisory, and proceeds-with-warning on timeout, so a flaky harness can never block a release indefinitely (#1683).
PhysicsNodeno longer clobbers or destroys the caller's existingNode.onFramecallback — it now saves the prior callback, chain-calls it each frame, and restores it on dispose (#1694). - Web: glTF animations now play instead of freezing at t=0,
OrbitCameraController.dispose()detaches its DOM listeners, andSceneView.destroy()releases leakedLightManagercomponents (#1697, #1698, #1700). Android TV demo: D-pad controls now work on launch — the rootBoxisfocusable()and requests focus on first composition so key events reach theonKeyEventhandler. - PhysicsDemo: each falling body now gets its own
ModelInstancespawned from a sharedModel, so every streamed crash-test mesh renders instead of only one (#1706). - VideoDemo no longer auto-plays the video if the user tapped Pause before the player became ready — the prepared callback now honours the user's desired playback state (#1707).
- Play Store deploy now self-heals a corrupt release AAB. A truncated or zero-byte App Bundle from a flaky CI runner (which silently cost the v4.6.0 and v4.6.1 store releases, #1412/#1415) used to sail past gradle's exit 0 and only blow up at upload. The
Build release AABstep now verifies the artifact is a readable zip and rebuilds once from clean before aborting, so a transient I/O flake no longer loses a release.SceneViewno longer triggers "Modifying state during view update" Xcode runtime warnings —appliedMainSlot,appliedFillSlot, andappliedSkyboxResourceare now held in a private reference-type cache class rather than individual@Stateproperties, so mutations insideRealityView.update:are invisible to SwiftUI's state-change detection.
Tests¶
- Android demo: wired the 12 live-only AR demos to honour
DemoSettings.arPendingPlaybackFile. A new sharedrememberArPlaybackDataset()helper resolves the--es ar_playback_file <path>deep-link extra (set by the autonomous AR replay device-QA harness) into theARSceneView(playbackDataset = …)parameter. Previously onlyar-record-playbackconsumed the extra, so the harness could only grade the other AR demosalive; they can now graduate toreplayedwith frame-indexed assertions. When the extra is absent — i.e. every normal launch — the helper returnsnulland the demos behave exactly as before, so there is no live-AR regression for real users (#1576).
Docs¶
- Corrected stale version references that the v4.10.0 release left behind — the docs landing-page "Latest Release" stat,
llms-full.txt's SceneView version line, the iOS deployment-target docs (now iOS 18 / macOS 15 / visionOS 2), the SwiftUI codelabs' SPM version rule, and an overstated web-demo changelog entry — and hardenedsync-versions.shto scan these files so future releases bump them automatically (#1693). - Refreshed the stale
ROADMAP.md(was pinned at v4.0.9) to v4.10.0 and resolved theCLAUDE.md↔sync-versions.shcontradiction overmcp/package.json— the Version Location Map now documentssceneview-mcpas an independent npm version track that must NOT be synced to the SDKVERSION_NAME(#1701, #1705).
v4.10.0 — 2026-05-17¶
Added¶
- Web demo catalog expanded with Lighting, Animation, Text, and Environment tabs (#1362). The
samples/web-demoplayground previously exposed only Models / Geometry / Physics / Settings — a small fraction of the SDK versus Android's ~39 demos. It now ships four new tabs in the demo app: Lighting adds and removes directional/point/spot lights, Animation loads self-hosted animated glTF models and drives keyframe playback, Text renders billboarded 3D text nodes, and Environment controls image-based lighting via spherical-harmonic presets, background color, and bloom strength. These tabs are wired against the demo's hand-vendoredsamples/web-demo/.../js/sceneview.jsviewer helper — they are a web-DEMO addition and do not change the publishedSceneViewJSKotlin/JS API surface. First slice of the cross-platform demo-parity effort; web-demo only. - Auto-fit camera framing (#1439): a new library-level helper in
io.github.sceneviewcomputes the orbit distance at which a model's bounding sphere exactly fills the viewport, regardless of the model's intrinsic glTF size.fitDistanceForBounds(bounds, verticalFovDegrees, aspect, padding)is pure trigonometry (yaw-invariant — fits the bounding sphere, not the raw box);CameraNode.frameToContent(node)/CameraNode.frameToBounds(aabb)reposition the camera in one call;verticalFovDegreesForFocalLengthandBox.toAabb()convert Filament's focal-length /Boxtypes;SceneAutoFitStateis a one-shot guard for use in aSceneViewframe loop. The Model Viewer demo now auto-fits its orbit radius to the displayed model — a 5 cm bee and a 5 m crate are framed identically without per-demoscaleToUnitstuning. Android-only for now; iOS already frames fromvisualBounds(#1026 / #1391). - Demo: Material Streaming (#1480). New Advanced-section demo in the Android sample app showing runtime texture/material streaming — a single loaded model whose surface material is swapped live from a chip picker (Polished Steel, Brushed Gold, Copper, Matte Plastic, Glazed Ceramic). The swap reassigns the node's Filament
MaterialInstanceviasetMaterialInstanceAt(...)with no geometry rebuild or model reload, lit by a studio HDR so the metallic/roughness contrast reads. Distinct from the PBR Materials demo (#1423), which streams a whole new model per chip. Material sets are bundled in-app so the demo renders offline; streaming the same material/texture data from a remote catalogue (Sketchfab material packs, a.ktxtexture-set CDN) is a documented follow-up. - device-qa: added the maestro iOS leg —
.maestro/ios/flows drive every iOS demo reachable via thesceneview://demo/<id>deep link insamples/ios-demolike a real user (custom-scheme launch, camera-orbit drag, tap, one screenshot per demo, crash assertion), with per-category subflows and launch-only smoke for AR demos (RealityKit AR cannot run on the simulator);ios-device-qa.shis the maestro wrapper that boots a simulator, builds + installs the demo and sweeps the simulator log for crashes (#1563). - device-qa: added
.claude/scripts/device-qa.sh— the autonomous cross-platform device-QA orchestrator that ties the four platform harnesses (Maestro Android, Maestro iOS, Playwright web, AR replay) into one unattended pass, boots the emulator/simulator each leg needs, builds + installs the demo app, and aggregates every platform's machine-readable verdict into a singledevice-qa-report.jsonplus a human-readable summary; it exits non-zero if any selected platform fails, is disk-aware (reusesdisk-gated-spawn-check.shand cleans build output between legs), and degrades a missing emulator/simulator/browser toskipped(treated as a failure under--ci). The release checkpoint (release-checklist.sh+ the/releaseskill) now blocks tagging on a greendevice-qa-report.json, and a path-gateddevice-qa.ymlCI workflow (also reused bynightly-ci.yml) runs the web and Android legs (#1566). - Device-QA emulator can now boot visible (windowed) via the opt-in
--windowflag orEMU_VISIBLE=1onsetup-ar-emulator.sh; the default stays headless and CI is unchanged (#1660).
Changed¶
- Android demo polish (#1443): demo-grid cards now carry a hairline
outlineVariantborder so their boundaries stay visible against the dark ParticleBackground; the Image Planes demo is staged as a three-picture wall gallery (framed procedural landscapes at varying depth and angle) instead of a single floating logo; the Billboard demo plants its billboard and fixed signs on a ground plane with an angled camera and explanatory caption so the orbit-time difference between the two node types is obvious. - device-qa: fixed
qa-android-demos.sh,ios-device-qa.shandar-replay-qa.shresolvingREPO_ROOTone level shy — they live in.claude/scripts/so the repo root is two levels up, not one. When invoked bydevice-qa.sh(whose CWD is not the repo root) the scriptscd'd into.claude/instead, so Maestro flow discovery found nothing ([qa] no such flow: .maestro/android/3d-basics.yaml). All three now deriveREPO_ROOTfrom${BASH_SOURCE[0]}/../..so every path (.maestro/...,./gradlew, the demo module) resolves regardless of the caller's CWD (#1585). sceneviewnode API honesty (#1598, #1599): verifiedMeshNodedoes not leak itsRenderableManagercomponent —RenderableNode.destroy()already releases the renderable built onentity(#1598 confirmed stale, no code change needed). Deprecated thePhysicsNode/PhysicsBodymassparameter — the Euler integration applies only gravity, which is mass-independent, somasswas a silent no-op; it is now@Deprecatedwith a clear message and the mass-free overload is the canonical one (#1599).- CI workflow hygiene (#1601, #1602): documented the device-qa.yml four-leg split (per-push web+android vs nightly-only ios+ar) and why
samples/ios-demo/**is deliberately absent from its path trigger; unified telemetry-ci.yml onnode-version: 20to match device-qa.yml and docs.yml; deleted the orphan top-leveldocs/screenshots/directory (a byte-identical, unreferenced duplicate ofdocs/docs/screenshots/, which MkDocs actually serves). - Device-QA harness now selects a single shared Android emulator RAM-aware and parallel-session-safe (#1647). Before booting,
setup-ar-emulator.shreuses any already-running emulator, gates a fresh boot on free host RAM, scales the-memoryflag to RAM headroom, and takes an advisory lock so concurrent Claude Code sessions cooperate on one emulator instead of each booting their own — fixing emulator resource contention and boot failures on RAM-constrained hosts. No multi-emulator pool: there is always exactly one shared emulator. - Removed a stale verification TODO in the Android demo's
AnimationDemo—ModelNode.playAnimation'sloopparameter is verified to be honoured correctly. (#1649) - device-QA harness (#1654): the emulator-selection layer is now a RAM-budgeted adaptive pool — it leases a free running emulator or boots a new one on a distinct
-portwhenever live host RAM safely allows (capfloor((free_RAM − headroom) / per-emu budget), clamped[1, EMU_POOL_MAX]), re-gates free RAM as a hard memory-safety check before every boot, reclaims stale per-emulator leases, and pinsANDROID_SERIALto the leased device — superseding the strict-single emulator of #1647 while keeping the floor at 1 on RAM-tight hosts. - Repo hygiene: stale
claude/*branches no longer pile up on the remote. TheAutomatically delete head branchessetting is now enabled, so every PR branch is dropped the instant its PR merges. Thecleanup-branches-worktrees.shbackstop was reworked to fetch PR status with two bulkgh pr listcalls instead of onegh pr viewper branch — the per-branch form fired hundreds of sequential API calls and timed the dailybranch-cleanupjob out before it could delete anything, which had let the remote grow to ~190 branches.
Fixed¶
- visionOS target of the
SceneViewSwiftSwift package now compiles (#1366): the deployment target is raised to visionOS 2.0 (RealityKit'sDirectionalLight/PointLight/SpotLightentities and per-entityshadowAPI are@available(visionOS 2.0, *)),SceneViewuses the cross-platformRealityViewContentinitializer on visionOS instead of the@available(visionOS, unavailable)RealityViewCameraContent, light components drop the visionOS-unavailableisRealWorldProxy:initializer parameter, and a newBuild Swift Package (visionOS)CI step inios.ymlbuilds the xrOS SDK on every iOS PR so this can't regress silently. - iOS: the
Multi-Model Parkdemo now frames all four streamed models centered and correctly sized. The previous fix translated the content root so its bounding-box centroid landed at the world origin, butMulti-Model Parknests its models under anAnchorEntity— RealityKit re-pins that anchor to its world target every frame, so the translation and the anchor fought each other and the framed centroid ran away to infinity, leaving a fully black viewport. The auto-framing pass now points the orbit camera at the content's world-space centroid instead of moving any scene node, which removes the feedback loop entirely. Framing is also computed from the union of every loaded model and re-runs until that union is stable, so partially-streamed scenes no longer latch early. Single-model demos (Model Viewer, Geometry) are unaffected (#1391, #1514, #1385). - Orbital AR demo now renders its four streamed planets. The demo loaded its resolver-staged GLBs through the two-argument
rememberModelInstance(modelLoader, String), which Kotlin overload resolution binds to the asset-path overload — so thefile://cache URI was handed toAssetManager.open, threwFileNotFoundException, and the four streamed planets stayednullwhile the four bundled-asset planets kept working. The streamed branch now loads the local file viaModelLoader.loadModelInstance, which understandsfile://URIs. Same root cause as the Multi Model demo fix (#1422). - React Native: bumped the iOS bridge podspec
SceneViewSwiftdependency from the year-old~> 3.4pin to~> 4.9, matching the published SPM tag the bridge code already targets. (#1512) - iOS test build:
AugmentedImageNodeTests.swiftfailed to compile under the iOS 26.2 SDK (#1515). TheAugmentedImageNode.ReferenceImage(name:image:physicalWidth:)initializer becamethrowsin #883, but the test still called it withouttry— the macOSswift testtarget stayed green only because it never built the iOS-gated test file. The throwing call sites now usetry(and a siblingCameraControlsTests.swiftnow importsRealityKitforBoundingBox), and theiOS CIworkflow'sxcodebuildsteps gainset -o pipefailso a failing test-build is no longer masked byxcpretty's exit 0. - CI: raise
Unit tests + coverageandCI Gatetimeouts (#1554). The full JaCoCo pass runs close to the old 30-min job timeout on a slow runner; it tipped over on a release PR and cascaded a confusing double-red. TheUnit tests + coveragejob timeout is now 45 min, and theCI Gateaggregator's internal poll deadline (50 min) and job timeout (60 min) comfortably exceed it so a slow-but-succeeding job is seen as completing. - iOS: detected ARKit planes now render as a subtle translucent overlay instead of an opaque bright-green debug fill that obscured the camera feed (#1557).
- Device-QA Android leg no longer hangs silently in CI (#1560). The leg ran 40+ minutes with zero output before the job timed out:
device-qa.shredirected the whole wrapper's output to a file shown only after it returned, andqa-android-demos.shbuilt the demo APK with Gradle-q(no output at all). The Android leg now streams live viatee, builds with--console=plain, and bounds the cold APK build and each Maestro run withtimeoutso a genuine hang fails fast with a clear diagnostic instead of eating the CI job budget. The job'stimeout-minutesis raised to 60 to give a legitimate cold build headroom. - Device-QA Android leg no longer aborts at Maestro flow-parse time (#1560).
.maestro/android/flows/demo.yamlpassed the demo deep link as adeepLink:sub-property oflaunchApp, but Maestro 1.39 has no such property — the flow failed to parse withUnknown Property: deepLinkbefore a single demo ran, sodevice-qa.sh --platform=android --fastreportedpassed=0 failed=1. The fix delivers the demo id andqa_modeflag aslaunchApparguments:instead, which Maestro maps to intent extras (--es demo <id>,--ez qa_mode true).MainActivityalready reads exactly those extras throughDeepLinkRouter.validate— the same closed-registry allow-list thesceneview://demo/<id>scheme uses — so demo routing and the deterministic-screenshot animation freeze are both preserved. Harness-only fix; no demo code changed. - Device-QA web leg — Geometry catalog test split per primitive (#1560). The single
Geometry tab — every primitive adds, recolours and renderstest looped over all four primitives in one test body and still overran even the tripledtest.slow()180s budget on GPU-less CI runners. It is now four independent per-primitive tests plus a dedicated Clear-All test, so each heavy WebGL-interaction pass gets its own budget. Every primitive is still exercised; harness-only change, no demo code touched. - Device-QA web leg no longer times out on GPU-less CI runners (#1560). Three Playwright catalog tests (
Models,Geometry,Settings) failed withTest timeout of 60000ms exceededon the GitHub Ubuntu runner: software-rasterised headless WebGL renders every Filament frame several times slower than a real GPU, so the looped model-load / geometry-add / render-quality-rebuild work overran the 60s budget. The demo itself was never hanging — it passed the same suite in seconds on a GPU-equipped host. Fix is in the harness, not the demo: the three heavy WebGL-interaction tests now calltest.slow()(triples their timeout) and the Models test waits for the demo's real load-completion signal (#loading-chipclearing) via a newwaitForModelChipIdlehelper instead of a blindwaitForTimeout(2500)— deterministic across fast local GPUs and slow CI runners, and faster locally because it no longer over-sleeps. The other 14 tests and the global 60s timeout are unchanged. - web-demo: self-host the curated catalog GLB models and the IBL environment, and screenshot-sample the canvas in the Playwright suite, so the browser viewer renders and the device-QA suite passes. The catalog previously loaded every model — including the initial scene model — from jsDelivr's gh-proxy, which returns HTTP 403 for large GLB blobs under
assets/;initSceneView()never resolved and the demo stayed stuck on its loading overlay. The 12-model catalog andneutral_ibl.ktxare now bundled undersamples/web-demo/src/jsMain/resources/and a localversion.jsonremoves the last 404, eliminating all external asset failures. ThesampleCanvastest helper now decodes a Playwright screenshot instead ofgl.readPixels, which returned all-zero pixels on Filament'spreserveDrawingBuffer:falsecontext even when the canvas was visibly rendering (#1573, #1586, #1362). - iOS demo: resolved Swift 6 concurrency warnings — the
OrbitalARDemoandDoublePendulumDemoper-frame timer closures now hop onto the main actor before touching main-actor-isolated scene state, fixing real data-race risks.DemoDeepLinkRegistry.destination(for:)is now@MainActor-isolated, andSceneViewDemoAppadopts the modern two-parameteronChange(of:)signature (#1574). - Web demo: self-host the Filament/SceneView engine (#1586).
samples/web-demo'sindex.htmlloadedfilament.jsandsceneview.jsfromcdn.jsdelivr.net— a jsDelivr hiccup 404'd both engine scripts and turned the Playwright device-QA suite red. Both files (plusfilament.wasm) are now bundled undersrc/jsMain/resources/js/and referenced by relative path, so they ship withjsBrowserDistribution. Engine init is also decoupled from the default model load: a flaky model miss now surfaces as a transient chip instead of a fatal "Failed to initialize" overlay. CI Gate no longer fails a PR when a Device QA workflow run was manually dispatched on the branch — Device QA check runs are excluded from the aggregator (#1588). - Auto-fit camera framing is now reachable, and Android multi-model framing no longer bunches in the corner (#1595, #1596): the #1439 auto-fit API (
SceneAutoFitState,frameToContent,frameToBounds) shipped with no caller —SceneViewnow exposes anautoFitContentparameter that drives it, moving the camera so the content fills the viewport regardless of the model's intrinsic glTF size. BothSceneAutoFitStateandSceneAutoCenterStatenow use a diagonal-stability gate (Android port of web'sAutoCenterGate, #1391 / #1540) instead of a first-frame latch, so an async model that finishes loading after a sibling already framed still triggers a re-frame. The gate also latches after a bounded number of passes so a perpetually-animated scene stops re-framing instead of fighting user interaction. - Web: guard
SceneView.loadModelagainst a use-after-free — a reloaded or destroyed model's pendingloadResourcescallback no longer touches the freedFilamentAsset(#1597). sceneview-webSceneView.loadModel(#1597): the auto-center pass no longer frames the scene on a model whoseloadResources()is still in flight (premature/wrong framing on an unreadable bounding box), and reloading the same model URL now destroys the priorFilamentAssetinstead of orphaning it on the GPU — mirroring theEnvironmentResourceTrackerleak-free-swap pattern from the IBL/skybox fix (#1496).assets/catalog.jsonsynced with bundled demo assets (#1603). Two assets that ship insamples/android-demo/src/main/assets/and are actively referenced by demos were missing from the catalog that declares itself the "source of truth for all demo assets across platforms": thethreejs_soldier.glbanimated character (used by OrbitalARDemo, AnimationDemo, MultiModelDemo, the AR view, android-tv-demo, and ios-demo) and thechinese_garden_2k.hdrPoly Haven environment (used by EnvironmentDemo). Both now have full registry entries withsource/author/license/sourceUrlprovenance andusedInarrays, matching the existing entry schema.- Device-QA AR leg no longer fails with a shell syntax error on its first CI run (#1608). The
arjob's ARCore sideload was an inline multi-lineif … fiblock in theReactiveCircus/android-emulator-runnerscript:. That action runs each line ofscript:as a separatesh -c, so the standaloneif … thenline aborted withSyntax error: end of file unexpected (expecting "fi")and env vars never persisted across lines. The sideload logic moved into a dedicated.claude/scripts/sideload-arcore.shhelper (ABI-aware ARCore APK resolution from the publicgoogle-arSDK release, honest non-fatal exit when ARCore is genuinely unavailable), and the workflow now invokes it as a single self-contained line — matching the workingandroidjob. EngineDestroyQueueno longer resurrects a queue after engine teardown (#1630).EngineDestroyQueue.of(engine)is backed by aWeakHashMap; aNode.destroy()arriving afterEngine.safeDestroy()(a disposal order that does happen) used togetOrPuta fresh, live queue against the now-dead engine — the enqueuedTexture/Streamwas then never drained (no render loop left) → GPU-memory leak and latent use-after-free if Filament reused the handle. Teardown now records the engine as destroyed and removes its live map entry; a staleof()returns an already-drained queue whoseenqueueTexture/enqueueStreamdestroy the resource immediately instead of queueing onto the dead engine. Dropping the live entry also fixes theWeakHashMap-value-strongly-references-key leak that pinned destroyed engines.- Web demo IBL no longer 404s on subpath deploys (#1631). The default IBL URL in the vendored
sceneview.jswas the absolute path/environments/neutral_ibl.ktx, which resolved correctly from a domain root but 404'd on subpath deploys (e.g./sceneview/), silently dropping image-based lighting to the synthetic SH fallback. It is now the relative pathenvironments/neutral_ibl.ktx, matching the self-hostedmodels/convention and working on both layouts. Additionally,.claude/scripts/validate-demo-assets.shno longer skips the entire vendoredweb-demoresources/js/tree — it now narrowly filters only the JSDoc placeholder literalmodel.glb, so a real broken asset literal in a future vendored js file is caught instead of silently passing. - Web
AutoCenterGatenow latches after a bounded number of framing passes (MAX_FRAMING_PASSES = 10), so an animated / skeletal / physics scene whose union diagonal jitters every frame stops re-centring the camera forever — parity with Android'sFramingGateceiling (#1633, #1629). - Device-QA Android leg — CI emulator stability (#1643). The Maestro flow ran correctly (app launch + camera-orbit swipes) but the CI emulator went offline mid-flow under the SceneView Filament 3D demo's GPU/RAM load. The
androidandardevice-QA jobs now boot the emulator with-memory 4096, andqa-android-demos.shretries the Maestro flow once when — and only when — the device drops offline (a genuine demo failure, where the device stays online, is not retried). - Daily Maintenance workflow now actually fires on its cron schedule (#1646).
.github/workflows/maintenance.ymlhad never run fromschedule:despite being marked active — its scheduled trigger had been registered against an account that is no longer active, so GitHub silently dropped every scheduled event for ~2 months (the workflow only ever ran when dispatched manually). Editing theschedule:block re-registers the cron under the current committing account. The cron is also moved off the congested top-of-hour (0 7→11 7UTC) so GitHub's scheduler no longer drops it in the hourly burst. All six maintenance jobs — dependency-version checks, stale-issue marking, the daily digest, agent-skill drift, CI health, and merged-branch pruning — now run unattended again. ARRecordernow converts theSurface.ROTATION_*constant passed asrecordingRotationinto degrees (0/90/180/270) before handing it to ARCore'sRecordingConfig.setRecordingRotation, which expects degrees — not the ordinal (0/1/2/3). Previously a 90° capture was recorded as1°, leaving AR datasets stored sideways. New publicARRecorder.surfaceRotationToDegrees(Int)exposes the mapping. (#1648)- Device-QA release gate (#1670): an all-skipped (or skipped-only) advisory leg is no longer aggregated as a hard
failed.device-qa.shnow splits the verdict by leg weight — only a non-passing required leg (e.g.web) blocks the gate (exit 1,releaseGate.verdict=blocked), while afailedor honestskippedadvisory leg (android/ar, e.g. the #1645ar-record-playbackskip on the CI emulator) surfaces as awarnand exits 0.release-checklist.shsection 14 then WARNs instead of FAILing for that case, so an honest environment skip no longer false-blocks a release tag. - Frame-deferred GPU texture destroy queue (#874).
ImageNode.destroy()no longer leaks its FilamentTexture, andViewNode.destroy()no longer risks a nativeSIGABRT(Invalid texture still bound to MaterialInstance) from freeing its texture/stream too eagerly — both now enqueue their GPU resources on a per-EngineEngineDestroyQueuethat destroys them a few rendered frames later, on the main thread, after Filament has reclaimed the boundMaterialInstance. High-churn UIs (feeds, infinite scrollers, particle emitters) that create many short-livedImageNodes perEnginelifetime no longer accumulate GPU memory. - DynamicSky demo now holds a "Loading helmet…" scrim until its model is ready, matching every other helmet-loading demo so no demo opens on a bare scene (#881).
- iOS demo: the Samples-tab full-screen demo cover now has an explicit Close button so a demo opened from the Samples list can always be dismissed back to the list (#1580).
- Play Store deploy now self-heals a corrupt release AAB. A truncated or zero-byte App Bundle from a flaky CI runner (which silently cost the v4.6.0 and v4.6.1 store releases, #1412/#1415) used to sail past gradle's exit 0 and only blow up at upload. The
Build release AABstep now verifies the artifact is a readable zip and rebuilds once from clean before aborting, so a transient I/O flake no longer loses a release.
Tests¶
- device-QA: wire the AR replay leg into CI (#1592).
.github/workflows/device-qa.ymlpreviously ran only the web (Playwright) and android (Maestro) legs; the AR replay harness (ar-replay-qa.sh+ARReplayHarnessTest, #1565) had no automated coverage, so the per-release device-QA pass effectively skipped AR. A newarjob boots an ARCore-capable emulator on the KVM-accelerated GitHub runner, sideloads Google Play Services for AR from the public google-ar SDK release, and runsdevice-qa.sh --platform=ar --ci, uploading thear-qa-summary.json/device-qa-report.jsonartifact like the other legs. - AR replay device-QA harness (
ARReplayHarnessTest+ar-replay-qa.sh) no longer reports a misleadingpasswhen the recorded ARCore session was never actually replayed. ARCore dataset playback needs camera-stream support the x86 software-GPU CI emulator does not provide, soar-record-playbackadvancingreplayedFrames: 0is now gradedskipped(with the reason surfaced) rather than greenalive.ar-qa-summary.jsongainsskipped/failedcounts and a per-demoreason;ar-replay-qa.shexits3and the device-QA AR leg recordsskipped— skips never count as passes (#1645). - Device-QA CI: prebuild the android-demo APK in a separate cached
build-android-apkjob and install the artifact in the emulator legs (no cold build on the 2-core emulator runner); the release gate now gradescontinue-on-errorlegs — a red advisory leg (android/ar) surfaces as a WARN instead of being silent or hard-blocking (#1652, #1651). - Device QA workflow (#1665):
workflow_dispatch(release-gate) runs now get a unique, non-cancellable concurrency group keyed ongithub.run_id, so a subsequent push tomaincan no longer cancel an in-progress release-gate Device QA run. Push-triggered runs still share apushgroup and auto-cancel stale runs. - iOS device-QA now screen-records each run (#1673).
ios-device-qa.shpreviously captured only one screenshot per demo; it now records the whole Maestro run viaxcrun simctl io recordVideo(h264, to keep clear of the hevc frame-glitch artefacts), bringing the iOS leg to parity with the Android leg's screen recording. The recording is strictly best-effort —recordVideoneeds hardware Metal, which CI VMs may lack, so a recording failure never fails the QA run — and is stopped with SIGINT so the.movfinalises cleanly. The file lands undertools/qa-screenshots/ios/(gitignored). - Web QA: pass
--enable-unsafe-swiftshaderto the Playwright Chromium runner (#1674). Chrome removed the automatic SwiftShader fallback for WebGL. On a GPU-less CI runner ANGLE has no hardware path and nothing to fall back to, so WebGL context creation would fail outright — the Filament.js viewer would never get a context and the web-demo test suite could go green-on-nothing. The flag re-enables the software rasteriser so headless CI keeps a real WebGL context. - Web device-QA now screen-records every test (#1674). The Playwright suite gains
video: 'on', bringing the web leg to parity with the Android and iOS device-QA legs so a 3D regression can be reviewed frame-by-frame. In headless Chromium the recording is software-rendered — the authoritative "did it render" assertion stayshelpers.ts:sampleCanvas(a compositor screenshot with a luminance-variance check); the video is for human review. Recordings land undersamples/web-demo/test-results/(gitignored).
Docs¶
- Device-QA harness documentation (#1567). Documented the autonomous cross-platform device-QA harness: a new "Device QA" section in
CLAUDE.md(how to rundevice-qa.sh, what each platform leg covers, where reports land, and the per-release-checkpoint mandate), aCONTRIBUTING.mdsubsection on adding/updating Maestro and Playwright flows when adding a demo, an orchestrator pointer in.maestro/README.md, and a Device QA section on the docs-site contributing page. Closes the final slice of umbrella #1560. - Refreshed stale doc references: corrected the Android demo count to 43, reconciled the Maestro AR catalog count, updated the version note in
CLAUDE.md, and repointed the iOS QA script reference. -
Align Apple platform minimums in docs with
SceneViewSwift/Package.swift(iOS 18.0, macOS 15.0). (#1621) -
expanded the web-demo playwright suite into full per-tab / per-demo qa coverage — exercises every models, geometry, physics and settings demo with camera interaction, canvas render assertions and console-error checks, and emits a machine-readable
web-qa-summary.jsonfor the device-qa orchestrator (#1564) - device-qa: added a maestro harness —
.maestro/android/flows drive all 42 android demos like a real user (deep-link launch, camera-orbit drag, tap, one screenshot per demo, crash assertion), with per-category subflows and amaestro.shauto-install helper;qa-android-demos.shis now a thin maestro wrapper (#1562). - device-qa: added an autonomous ar replay harness —
ARReplayHarnessTestdrives every augmented-reality demo through a recorded arcore session headless on the emulator (no physical device), asserts no crash, and emits a machine-readablear-qa-summary.json; thear-replay-qa.shscript is the orchestrator entrypoint that builds, runs and pulls the verdict (#1565). - device-qa: fixed the android (maestro) leg of the device-QA CI workflow failing every run with the opaque
qa-android-demos.sh rc=1 (flow=3d-basics). TheInstall Maestrostep (and themaestro.shauto-install helper) fetched the installer fromget.maestro.dev, which does not resolve — the canonical host isget.maestro.mobile.dev. Because the install ran ascurl … | bash, the curl DNS failure was masked by the pipe and the step passed falsely-green, so Maestro was never on PATH and the flow could not run. The installer URL is corrected and the workflow step now runs underset -o pipefailwith an explicittest -xon the binary, so a future install failure aborts loudly at the install step instead of surfacing as a misleading flow failure (#1560).
v4.9.0 — Cross-platform demo catalogs, web auto-center parity & teardown safety (2026-05-16)¶
Added¶
rememberPausableHeroYawgained an opt-inidleResumeMillisparameter: after the user stops interacting with the viewport, the hero auto-rotation gently resumes once the idle timeout elapses. Each gesture restarts the countdown, so the spin only comes back when interaction has truly stopped. Demos that omit the parameter keep the original pause-forever behaviour. Wired into the View Node demo. (#1440)- AR Orbital demo: an on-screen directional arrow now appears at the viewport edge whenever the chase target (the orbiting toy car) is outside the camera frustum, pointing the user toward it so they know which way to turn to catch it. The arrow is driven by a per-frame
projection · view · worldPointprojection that also handles the behind-the-camera case (#1482). - React Native & Flutter demo apps gain Materials / Animation / Environment demos (#1362). Part of the cross-platform demo-parity umbrella: the RN and Flutter sample apps showcased only a small slice of the bridge surface. The
react-native-demoapp adds three tabs — Materials (lit PBR vsunlitgeometry materials), Animation (auto-playing glTF clips viaModelNode.animation) and Environment (HDR image-based lighting plus theautoCenterContenttoggle) — and its bottom tab bar is now horizontally scrollable so the catalog can keep growing. Theflutter-demoapp gains a dedicated Demos tab with four runnable per-feature scenes — Materials (GeometryNode.unlit), Model Animation (loadModelwith animated Khronos assets), Environment (setEnvironment+setAutoCenterContent) and Camera Modes (setCameraControlMode) — complementing the existing flat "Bridge Features" reference checklist. Every demo uses only APIs the Fabric / PlatformView bridges actually expose; no dead UI for un-bridged features.
Changed¶
- AR demo: Strengthened the Record & Playback demo's end-of-recording UX. After Stop, the saved-recording callout now explains where the file lives and that it is a standard MP4 carrying ARCore data tracks, and offers Replay, Share, Open (play as a normal video) and Export-to-Downloads. The just-recorded file is highlighted with a "Just recorded" badge in the Playback list so it is obviously discoverable. (#1438)
- Double Pendulum demo reworked with an original SceneView visual identity and fixed camera framing (#1481). The Android demo keeps the genuine shared-KMP double-pendulum physics but is restaged as a ball-and-rod "Orbital Pendulum": glossy weighted bobs drawn at each link's actual point mass, an asymmetric long-lead / short-trailing arm ratio, a SceneView brand-token palette (primary blue → gradient violet), a warm studio backdrop, and an off-axis key/rim light rig — so it reads as SceneView's own demo rather than a port. The camera now auto-frames the full reachable swing envelope (it targets the swing-disc centre and backs off proportionally to the arm reach), fixing the poorly-aimed framing flagged in QA.
Fixed¶
- AR camera background no longer renders washed-out / low-contrast.
createARViewwas usingToneMapper.Linear, but the camera-stream shader'sinverseTonemapSRGB()pre-applies an inverse Filmic tone-map curve (Inverse_Tonemap_Filmic(pow(c, 2.2))) that only round-trips back to the original camera pixels when the View re-applies the matching Filmic tone mapper. WithLinearthe inverse curve was left uncancelled, flattening the live camera feed. The AR view now usesToneMapper.Filmicand keeps bloom/AO off so the background is faithful to the real camera image (#1434). - AR placed content no longer vanishes on transient plane loss, and placed models no longer flash black (#1435). In the Android demo's
ARPlacementDemoandARInstantPlacementDemo, eachAnchorNodenow keeps rendering its model while the ARCore anchor isPAUSED(it holds its last known pose) instead of disappearing the moment the camera looks away from the plane — content only hides on a permanentSTOPPEDanchor. Newly placed models are also kept hidden for a short settle window after loading so Filament finishes uploading their textures, eliminating the black flash on placement. Behaviour is centralised in the newdemos/internal/ArPlacementhelper with JVM regression tests. - AR Face Mesh demo: the face mesh now actually tracks.
Session.Feature.FRONT_CAMERAonly makes the front camera eligible — the session stayed on the default BACK camera config, soAugmentedFaceMode.MESH3Dproduced zero trackables and no mesh ever appeared. The demo now passessessionCameraConfig = ::frontCameraConfigso ARCore opens the selfie camera. Added a publicfrontCameraConfig(session)helper inarsceneviewfor any Augmented Faces consumer (#1436). - Image Tracking (Augmented Images) demo now shows an in-app "what to scan" card displaying the actual reference target image, so the user knows exactly which image to point the camera at. The card auto-collapses to a chip once an image is recognised and can be re-expanded by tapping it (#1437).
- Animation demo model no longer renders as a black silhouette against the HDR environment (#1468). The demo's
rooftop_nightskybox renders at full HDR luminance, but the image-based light defaulted to only 5,000 lux — half SceneView's balanced 10k default — so the soldier read as unlit against the bright sky. The default IBL intensity now matches the balanced 10k default; the slider still lets users dial down for a darker, atmospheric look. - Video demo: the viewport background is now a clean neutral black instead of a light near-white wash (or a stale gradient leftover from the previous screen). The demo loaded its HDR environment with
createSkybox = false, leaving anullskybox — Filament does not clear background pixels without a skybox, so the uncleared swap-chain buffer leaked through and broke the dark theme every other demo uses. The HDR IBL is now paired with an explicit opaque black skybox (#1469). - Text Nodes demo: pulled the camera back so the top "Hello SceneView" label is no longer clipped at the top viewport edge in the default framing.
- Gesture Editing demo — the X/Y/Z axis gizmo is now bounded to the model instead of running off all four screen edges (#1471). The world-origin axis gizmo was 1 m long while the helmet renders at 0.3 m, so with the camera framed on the small helmet each axis tip extended well past the viewport and looked like an infinite debug line. The gizmo length is now derived from the model scale (1.5× the helmet's
scaleToUnits), keeping each axis just longer than the model's bounding box as a clear, bounded reference. - ViewNode demo no longer shows a black viewport for several seconds on entry (#1472). The scaffold's first-frame scrim dismissed on the SceneView's very first Filament frame, which arrives almost instantly because the quads carry no asset to load — but a
ViewNoderenders its embedded Compose card to an off-screen window and uploads it as a texture only a handful of frames later, leaving two black quads exposed. The demo now holds the loading scrim for a short frame warm-up so the embedded card texture is uploaded before the scrim cross-fades out. - Android demo: AR demos (Record & Playback, Terrain Anchors) now show a "Starting camera…" spinner overlay while ARCore initializes the camera, instead of a bare black viewport that read as a frozen/broken screen. The overlay clears on the first delivered AR frame.
- AR Record & Playback demo: the "REC" elapsed-time pill is now inset below the system bars so it no longer overlaps the status bar / notch / camera cutout.
- AR Image Stabilization demo: the EIS toggle now actually switches stabilization on and off (#1475). The demo previously rebuilt the entire
ARSceneView(key(eisOn)) on every toggle, which tore down the ARCore session and silently invalidated the placed helmet anchor — the demo's only reference object vanished the instant the user flipped EIS, so an "EIS ON" state was never visible. The toggle now reconfiguresConfig.ImageStabilizationModelive viaSession.configure(a runtime-mutable flag), keeping the session, tracking, and anchor intact. The status pill reflects what ARCore actually applied — "EIS ON", "EIS OFF", or "EIS UNSUPPORTED" when the device or recording can't do EIS — instead of a stuck "OFF". Android demo app only. - AR Instant Placement demo: replaced the tall per-model status column (which overflowed the top third of the viewport and overlapped placed models) with a single compact badge for the most recently placed model, and gave the "Clear All" button a solid filled background so it is legible over the camera feed.
- VideoNode / MaterialLoader: hardened MaterialInstance teardown against native crashes (#1539, follow-up to #1497). The
VideoNode.materialInstancesetter now drains the frame pipeline before freeing the superseded MaterialInstance — previously it was freed while the external video texture was still GPU-bound, the sameInvalid texture still bound to MaterialInstanceSIGABRT #1497 fixed fordestroy().MaterialLoader.destroyMaterialInstancenow removes the instance atomically, so two threads can no longer both pass the tracking guard and double-destroy the same native MaterialInstance. sceneview-web: multi-model scenes no longer render bunched in a corner, and the camera now auto-fits content size (#1540).SceneView'sautoCenterContentpass latched on the first render frame with non-degenerate bounds, so an async model that finished loading after a sibling had already centred never re-centred — the multi-model regression #1391 fixed on iOS. The webAutoCenterGatenow ports the iOS #1391 logic: it re-frames on every union-diagonal growth and latches only once the union diagonal is stable across consecutive frames, so a deferred async model always pulls the framing back to the combined extent. The pass also now callsfitToModels()to auto-dolly the orbit camera to the content size — previously the web viewer only auto-centred and never auto-fit, mis-framing very small or very large models. The union-AABB computation is shared between the auto-center path andfitToModels()(no duplicate read).- Web demo tab navigation no longer double-fires on every click (#1541). Tab buttons were wired twice — once by the inline JS in
index.html(the shipped runtime, loaded via CDNsceneview.js) and again by a duplicatesetupTabs()in the Gradle-compiled KotlinMain.kt, which is not referenced by the page. The dead Kotlin tab path has been removed so each.tab-btnclick runs a singleswitchTabhandler. - Docs: reconciled
samples/README.mdwith the actualDemoRegistry— corrected the android-demo demo count (now 42: 28 non-AR + 14 AR), fixed the tab list (Explore, AR View, Samples, About), and removed rows advertising demos that don't exist (gltf-camera,ar-point-cloud,autopilot-demo). - Docs: fixed HDR asset paths across
samples/recipes/(environment-lighting.md,multi-model.md,editable-model.md) to match the bundledenvironments/*_2k.hdrfiles, and corrected the Flutterfeatures_page.dartsnippet to reference the realenvironments/studio_small.hdrasset. CI Gateno longer flips red on fork PRs that are merely awaiting maintainer approval. GitHub reports such checks with conclusionaction_required; the aggregator now treatsaction_requiredas pending-equivalent (it keeps waiting for the run to be approved-then-completed) instead of counting it in the failed set. It also added a name-based core-check guard so the gate cannot exit green before every always-runci.ymlcheck (Detect changed paths,Repo hygiene checks,Quality gate (full)) has registered for the head SHA — closing a race where a slow-to-register workflow could be missed. The guard is a no-op for genuine docs-only PRs (whereci.ymlis path-filtered out entirely), so light PRs are never blocked (#1543).- Docs version staleness fixed (#1544).
CLAUDE.md's "Latest release" block claimedv4.4.0and instructed AI sessions to treat it as the latest version — 4 minors stale (repo is4.8.0); it is now version-agnostic and points atgradle.properties:VERSION_NAMEas the single source of truth.README.md's SwiftPM install snippets (from: 4.4.0/(SPM, from 4.4.0)) are bumped to4.8.0, and a broken intra-repo anchor inCLAUDE.mdis corrected.sync-versions.shnow also recognises the unquotedfrom: X.Y.ZSwiftPM prose form used inREADME.md, so this drift is caught automatically on future releases.
v4.8.0 — Bottom-sheet settings, web & RN bridge fixes (2026-05-16)¶
Added¶
- Demo settings bottom sheet gains a header, "Reset" button and status-aware peek chip (#1154).
DemoScaffold(Android demo app) now renders a pinned sheet header; demos can opt into anonResetSettingscallback to show a "Reset" text button that restores their defaults, and into apeekHeaderstring so the closed peek chip can surface a short live status (e.g. "3 anchors placed") instead of the generic "Settings" label. Drag-down-to-dismiss now fires a subtle haptic tick, and the previously hardcoded chip/FAB labels moved to string resources.FogDemowires up the new reset button as the reference adoption. Part of the #1154 umbrella (Stage 3 polish, Android slice).
Fixed¶
- Play Store listing sync no longer marks a successful deploy red (#1386). The
Sync Play Store listing (en-US)job inplay-store.ymlis nowcontinue-on-error: trueand swallows a403 Forbidden(missing 'Edit store listing' permission) with a warning. The AAB build/publish jobs stay strict, so the listing-text sync is best-effort and can never block a release. - iOS
SceneViewnow frames multi-model scenes by the union of all loaded content (#1391). The fit-to-bounds camera pass added in #1385 framed a single content entity and latched on the first model that loaded, so multi-model demos likeMulti-Model Parkrendered their streamed models bunched in a corner of an otherwise empty viewport. The pass now computes the union axis-aligned bounding box of every content entity, centres the camera on the union centre, dollies to fit the whole union, and re-frames as each streamed/async model finishes loading — latching only once the union stabilises. Single-model demos are unaffected. Part of #1373. - Model Viewer demo: the top-right "Streaming…" asset-source pill now clears to "Streamed" once a streamed model finishes loading, instead of staying pinned for the whole session. The streamed model instance is now loaded from a stable composable slot so the load-completion state invalidates the chip correctly (#1464).
- Scene Gallery demo no longer shows contradictory status labels: the top-right asset-source chip now stays "Streaming…" until the model is fully loaded, matching the centre loading overlay, instead of flipping to "Streamed (cached)" the moment the file path resolves (#1465).
- Light Types demo: re-scaled the backdrop wall from 3 × 2.4 m down to 1.6 × 1.2 m and re-centred it on the helmet. The oversized quad previously filled ~⅔ of the viewport with a hard diagonal top edge, cramming the model into the lower-left corner (#1466).
- Movable Light demo: the draggable yellow light handle is now persistently visible. The light's orbit radius was reduced from 1.5 m to 0.75 m and its elevation clamped to ±50° so the handle stays inside the fixed camera's frustum for the whole drag, instead of swinging off-screen for most of the orbit. The handle sphere is also slightly larger (radius 0.09 m) so it reads as a clear, aimable target (#1467).
sync-versions.shno longer bumps the Flutter/RN plugins' consumed SceneView dependency (#1494). Theio.github.sceneview:(ar)sceneview:X.Y.Zcoordinate in the Flutter plugin and React Native bridge Gradle files is a dependency on the published Maven Central artifact, so it must lag to the last released version — pointing it at the in-flight release broke theBuild flutter-demo APKCI check during v4.7.0. The script now reports these consumed-dependency coordinates WARN-only (never MISMATCH) and excludes them from every--fixsweep, while the plugins' own package versions still bump correctly.- Web
SceneViewno longer leaks IBL + skybox GPU resources (#1496).sceneview-web'sSceneView.loadEnvironmentcreated a FilamentIndirectLightandSkyboxbut never tracked the handles —destroy()left both resources allocated on the GPU, and a 2ndloadEnvironment/loadDefaultEnvironmentcall overwrote the scene's environment while orphaning the previous handle. The handles are now tracked by anEnvironmentResourceTracker, the previous IBL/skybox is destroyed before a replacement is bound, anddestroy()detaches and destroys both. - CI hygiene cluster 2 (#1500). Narrowed
app-store.yml's tag trigger fromv*to the strictv[0-9]+.[0-9]+.[0-9]+semver glob so pre-release or stray tags can no longer fire an App Store deploy;ci-gate.ymlno longer treats astalecheck conclusion as a failure (astalerun is superseded, not broken) and now refuses to report green until at least one non-self check run has registered, closing a warm-up race;render-tests.ymlpathsnow also matchesbuild.gradle*/settings.gradle*/gradle.propertiesso renderer-affecting build-script changes are not skipped; andrelease.yml's dead cross-rundokka-api-docsartifact upload was removed (download-artifactonly resolves same-run artifacts). - React Native Android bridge now compiles against the current SceneView 4.7.0 (#1501).
react-native/react-native-sceneview/android/build.gradle.ktsdepended on the year-oldio.github.sceneview:sceneview:3.6.0/arsceneview:3.6.0— pre the v3.6Scene-to-SceneViewcomposable rename and missing every 4.x feature — while the published@sceneview-sdk/react-nativepackage is versioned 4.7.0. The Maven coordinates are bumped to the last-published4.7.0(a consumed dependency, so it tracks the released artifact per #1494). The stalecompose-bom:2024.06.00is aligned to the repo's2026.05.00, and the obsoletecomposeOptions { kotlinCompilerExtensionVersion }block is replaced by the Kotlin 2.x Compose Compiler Gradle plugin (org.jetbrains.kotlin.plugin.compose). The bridge Kotlin already targeted the 4.x API surface, so no source migration was needed — only the build configuration lagged. - Refreshed stale docs flagged by the post-v4.7.0 audit (#1502).
docs/docs/desktop-filament.mdpinned Filamentv1.70.1throughout (clone/download archives and thefilament-android:1.70.1AAR), but the repo runtime is1.71.0and the committed.filamatblobs are v71 — all six references now read1.71.0.CLAUDE.md's pre-push unit-test command mixed:sceneview:testwith:arsceneview:testDebugUnitTest; both modules now usetestDebugUnitTest, consistent with CI. The fully-completeddocs/v3.6.0-roadmap.md(all 14 issues done, predating the 4.x line) is moved todocs/archive/v3.6.0-roadmap.md. - web-demo:
Main.kt's stale tab switcher fixed (#1503). The Kotlin/JSswitchTab()toggledpanel-viewer/panel-geometry, butindex.htmlshipspanel-models,panel-geometry,panel-physics,panel-settings— a drift that left 3 of 4 panels unreachable from that entry point, withcurrentTabalso defaulting to the non-existent"viewer".switchTab()now toggles all four shipped panels andcurrentTabdefaults to"models". The shippedindex.htmlinline-JS demo (the actual runtime path) already wires all four tabs plus the Double Pendulum physics and Settings panels correctly. The stalesamples/web-demo/README.mdand the Playwright suite were also refreshed to match the shipped 4-tab UI, and a newrender.spec.tstest clicks every.tab-btnand asserts the matchingpanel-*becomesactive. - android-demo: honest demo-status badges, correct AR icon, and a fixed Blender recipe path (#1504). The
DemoStatusenum andStatusChipUI were fully built, but all 41ALL_DEMOSentries used the defaultWorking, leaving the honest-badge feature inert. The four ARCore Geospatial / Cloud Anchor demos (ar-cloud-anchor,ar-streetscape,ar-terrain,ar-rooftop) — which all require a Cloud project API key not wired into default builds — are now markedKnownIssue, so they surface a "Preview" chip instead of lying as all-green.ar-instant-placementno longer uses theHourglassEmpty('coming soon') glyph despite routing to a real working demo — it now uses the placement-themedBolticon. Finally,samples/recipes/blender-to-sceneview.mdno longer points at the non-existent concrete pathsamples/android-demo/src/main/assets/models/car.glb; it now shows an illustrative relative path with a note clarifying thatcar.glbis the reader's own exported file and pointing at the real sample models. VideoNode.destroy()no longer risks a native SIGABRT (#1497).destroy()freed the externalTexture/Streamimmediately after destroying theMaterialInstancethat referenced them — the exact ordering that triggers Filament'sInvalid texture still bound to MaterialInstanceabort, since MaterialInstance reclamation is coupled to the render loop rather than to thedestroy()call site. Teardown now drains the frame pipeline (Engine.drainFramePipeline()) between destroying the MaterialInstance and freeing the external texture/stream, mirroring the safe pattern documented on the siblingImageNode. AddsVideoNodeTestpinning the teardown ordering.
v4.7.0 — Bridge expansion, slimmer APK & demo-app polish (2026-05-16)¶
Added¶
- Animated 3D particle background on the Samples home (#1488). The android-demo Samples tab now renders a subtle, on-brand particle field behind the demo grid — a
SceneViewscene of drifting low-poly spheres with a slow auto-orbiting camera, seeded per launch. A first visual experiment that dogfoods the SDK on the app's own home screen; tuning constants live inParticleBackground.kt.
Changed¶
- Daily maintenance digest in CI (#1303).
maintenance.ymlnow runs a report-only mirror of the/maintainskill — a new.claude/scripts/maintenance-report.shproduces a structured table (CI health, open issues/PRs, dependency drift, version sync, agent-skill drift, release decision) emitted to the workflow step-summary and an auto-updated tracking issue. The script is strictly read-only, retries transient API failures, and never blocks the workflow. - Consolidated the three PR CI workflows into one (#1370).
ci.yml,pr-check.ymlandquality-gate.ymlare merged into a singleci.ymlwith ONEchangespath-detection job gating every downstream job (build,lint,unit-test,web-desktop,flutter-demo,compile-kmp,repo-hygiene,quality-gate). Eliminates the duplicatedorny/paths-filterrun and two redundant checkout + JDK + Gradle-cache-restore chains per PR. TheCI Gateaggregator is unchanged — it polls the Checks API and treatsskippedas passing.Closes #1370. - Dropped the dead
desktop-democompile step fromci.yml(#1396). Theweb-desktopjob ran:samples:desktop-demo:compileKotlinDesktopwithcontinue-on-error: truepermanently, so it could never fail the build — it only burned runner minutes. Sincesamples/desktop-demois a deliberate Compose Canvas wireframe placeholder (it does not use SceneView or Filament), the step was removed along with the now-redundantsamples/desktop-demo/**path filter. The job is renamed "Build web targets" to match what it actually does. - Android demo APK slimmed ~36% (#934). Bundled demo assets are now compressed with no visible quality loss: the 7 HDR environments are downsampled 2K → 1K in linear-radiance space (energy-preserving 2×2 box average, 42 MB → 11 MB) and the 6 GLB models use
KHR_draco_mesh_compressiongeometry plusEXT_texture_webptextures (21 MB → 9.5 MB), both decoded natively by Filament's bundledgltfio.android-demobuild.gradlealso drops duplicate transitive licence/metadata files viapackaging.resources.excludes. Release APK: 98.6 MB → 62.7 MB. No code or API change — all 37 demos load the same asset paths. - Playground model picker now shows thumbnails (#953). The website playground listed 30+ models in a plain
<select>dropdown — a weak marketing surface next to Sketchfab/Babylon. The picker is now a visual thumbnail gallery: each model is a 256×256 self-hosted WebP preview (rendered offline, ~200 KB total for all 34, lazy-loaded), grouped by category, with light/dark styling fromDESIGN.md. The native<select>is kept hidden as the accessible source of truth so all existing preview logic and keyboard access are unchanged. The "Open in Cursor/Windsurf/Copilot" AI links are relabelled "Open Cursor/Windsurf/Copilot" since those tools have no prompt deep-link and only open their homepage.
Fixed¶
- Demo Settings sheet no longer dismisses itself instantly (#1420). The
DemoSettingsLayerbottom sheet treated its initialSheetValue.Hiddenstate as a dismissal, slamming the panel shut before it could animate open — making the Settings controls dead in every demo.Hiddenis now only honoured as a dismiss once the sheet has actually settled in a shown detent. - Light Types demo no longer renders an empty black scene (#1421). The demo composes a helmet, an off-centre backdrop wall, and a light-source marker;
autoCenterContentcentred the union of all three, shifting the helmet far off the hero camera's fixed orbit pivot. The demo now passesautoCenterContent = falseso each node keeps its authored position and the camera frames the lit helmet as intended. - Multi Model demo no longer hangs on "Loading 4 models…" (#1422). The demo loaded its four resolver-staged GLBs through the two-argument
rememberModelInstance(modelLoader, String), which Kotlin overload resolution binds to the asset-path overload — so thefile://cache URI was handed toAssetManager.open, threwFileNotFoundException, and every model instance stayednull. The demo now loads the local file viaModelLoader.loadModelInstance, which understandsfile://URIs, so the scene reaches a rendered state. - Streamed demos no longer hang forever on their loading spinner (#1423).
SketchfabAssetResolverstaged the offline fallback (and network downloads) by opening an output stream directly on the shared cache path. When a demo resolved the same model from bothprefetchAlland its per-slugproduceStateat once, the two writers interleaved and left a truncated GLB on disk that poisoned the cache permanently — the PBR Materials, Multi Model and Scene Gallery demos stayed stuck on "Streaming material…" / "Loading…" indefinitely. The resolver now stages into a per-call temp file and atomically renames it into place, and re-stages any cached fallback whoseglTFmagic header is missing so an already-poisoned cache self-heals. - Explore tab no longer crashes and loads thumbnails reliably (#1424).
AsyncNetworkImagedecoded Sketchfab thumbnails at full resolution intoARGB_8888bitmaps; ~30 oversized images at once exhausted the heap and the tab crashed withOutOfMemoryError— which therunCatchingfetch path never caught because anErroris not anException. Decoding is now downsampled via a two-passBitmapFactoryinSampleSize, the fetch path catchesThrowableand degrades to a silent placeholder, the in-memory cache is bounded by entry count (so one oversized bitmap can no longer evict every other thumbnail and cause the "appears one time in ten" flicker), and the shared OkHttp client now has connect/read/call timeouts so a stalled CDN connection can't pin an IO thread and leave carousels spinning. - Edge-to-edge insets in the Android demo (#1425). Removed the large empty gap above the "Samples" tab header — the nested
LargeTopAppBarno longer double-counts the status-bar inset already applied by the rootScaffold. The in-app "Update ready / Restart" banner is now z-ordered above every screen and inset below the status bar, so it is no longer clipped behind a demo's top app bar. - Flat geometry no longer vanishes when rotated (#1426). The Geometry Primitives plane and the Text Nodes labels now use double-sided materials, so they stay visible when their back face turns toward the camera instead of blinking out under single-sided culling.
- Camera feel re-tuned across 3D model demos (#1427). The default camera now sits further back (
DefaultCameraNodeZ2.0→2.75, Y0.3→0.4) so origin-placed models are no longer framed too tight. Orbit/pan sensitivity is reduced (orbitSpeed0.005→0.003) so finger drag tracks the model more calmly, and pinch-zoom is made more responsive (DEFAULT_PINCH_ZOOM_SPEED1/30→1/18) so zooming no longer feels sluggish. - Camera Controls demo: the Free Flight camera mode no longer renders a black
viewport on launch and is now usable on touch devices. Free-flight previously
spawned the camera at the origin — inside the helmet model — and offered no touch
gesture to translate (Filament drives flight movement from held keys). The demo now
sets
flightStartPositionto the framed home position and shows an on-screen movement pad (forward / back / strafe / up / down) wired to the manipulator's key controls. Each mode also shows a short usage hint. (#1428) - Gesture Editing demo — rotate/scale gestures now actually transform the helmet (#1429). The 60 Hz live-transform poll was recomposing the whole demo (and the
SceneView) every frame, which re-ranSceneScope.ModelNode's declared-transformSideEffectand reverted every gesture-driven rotation before it was visible. The poll is now isolated in its ownLiveTransformOverlaycomposable so gesture transforms persist. The "Moving camera" gesture-mode pill was also moved off the top-center anchor to the top-start corner so it no longer overlaps the top-end pos/rot readout card. - Collision & Hit Test demo — hit-test fires outside object bounds (#1430). The demo hand-authors five shapes and a camera manipulator pinned to their row, but left
autoCenterContentat itstruedefault. Auto-centering recentred the shapes to the scene origin, off the camera's orbit pivot — so the camera orbited empty space and taps no longer lined up with the rendered shapes. DisabledautoCenterContentfor this demo (same root cause as the LightingDemo #1421 fix) so each shape keeps its authored position and collision matches what is shown. - Custom Mesh auto-rotate no longer stops on a stray tap (#1431). The "Auto-Rotate" molecule demo silently paused its spin the first time the viewport was touched, so it looked like the rotation stopped on its own. Rotation is now continuous and controlled solely by the explicit Auto-Rotate switch.
- Lines & Paths — the Stroke Width slider now visibly rebalances the lines (#1432). Moving the line-width control in Settings appeared to do nothing. The per-point stroke beads now have a fixed base geometry and are driven by
Scale— a transform thatSphereNodere-applies unconditionally every recomposition — so dragging the slider tracks every bead every frame with no vertex-buffer rebuild. The beads were also rebalanced (smaller base radius, denser run along the line) so the default reads as a clean medium stroke instead of a string of oversized spheres, and the line beads now overlap into a continuous tube at higher widths. - Scene Gallery demo no longer appears stuck in a loop (#1433). The four gallery chips carried unverified placeholder Sketchfab uids, so every chip fell back to a bundled model — and two chips ("Reading Lamp" + "Wooden Chair") shared the same fallback GLB, making the chips look inert. Each gallery entry now points at a distinct bundled model with an honest label, so switching chips visibly changes the rendered model.
- Physics demo: the rigid-body simulation now actually runs — the
SphereNodecomposable no longer re-pushesposition/rotation/scaleto the node on every recomposition, which was clobbering the per-frame position written byPhysicsBody(the same fix already applied to the bareNodecomposable). Spheres now drop, bounce, and settle as intended (#1463). - Physics demo: re-framed the camera so the grey ground plane is vertically centred in the viewport instead of being shoved into the bottom third with its near edge clipped.
- AR placement demos no longer drop the helmet face-down (#1477). The bundled Khronos DamagedHelmet GLB ships a residual +90° X root rotation from its Blender export, which landed it nose-into-the-floor when placed under an ARCore plane anchor. The Cloud Anchor, Tap to Place, and Depth Occlusion demos now apply a shared
-90°X correcting rotation at placement time so the helmet stands upright, visor forward. Other bundled cycle models are unaffected. - Playground
Duckmodel no longer 404s (#1487). Added the self-hostedDuck.glbasset so the playground'sDuckmodel option and the Spring Physics example load correctly instead of issuing a 404 for the missing file.
Added¶
- Flutter bridge:
addGeometry/addLightare now rendered natively on Android (#909). These twoSceneViewControllermethods previously returnedresult.success(null)without drawing anything — the Flutter demo's feature badges did not reflect that. The AndroidSceneViewPluginnow appends to reactivegeometryNodes/lightNodesCompose state lists, socube/box,sphere,cylinderandplaneprimitives anddirectional/point/spotlights render in bothSceneViewandARSceneView, matching the React Native bridge. Material instances are cached per(color, unlit)and released on dispose. The Flutter demo's GeometryNode / LightNode feature cards are re-labelled "Android only" (the iOS RealityKit port stays tracked under the #909 umbrella). First Dart unit tests for the plugin (data-class serialization + controller attach guards) were also added. - Regression tests for the
samples/commonshared helpers (#972). TheLifecycleAwareLaunchedEffect(#936) andrememberMaterialInstance/rememberUnlitMaterialInstance(#937) helpers shipped with no tests. New JVM/Robolectric suites pin their contracts:LifecycleAwareLaunchedEffectTestdrives a realTestLifecycleOwnerto assert the body cancels ononStopand re-runs from the top ononStart, andRememberMaterialInstanceTestfails if a future edit putsmetallic/roughness/reflectanceback into theremember(...)key (the 60 HzMaterialInstancechurn caught by the #937 review).:samples:common:testDebugUnitTestis now wired into the CI unit-test step.
Changed¶
- Render tests are no longer orphan
@Ignore'd code (#912). The five headless Filament render-test classes (RenderSmokeTest,LightingRenderTest,GeometryRenderTest,VisualVerificationTest,DemoParametersRenderTest) were wholly class-level@Ignore'd — compiled but never executed, so they could never catch a regression and silently drifted. They now use a runtime capability gate (RenderTestCapabilities.assumeGpuReadbackAvailable()): the tests run on a hardware-GPU runner that opts in via-Pandroid.testInstrumentationRunnerArguments.gpuReadback=true, and cleanly skip (JUnit assumption, not orphan, not failure) on the SwiftShader / Apple-Silicon emulator where Filament's asyncreadPixelscallback never fires (the harness limitation tracked in #803).Closes #912.
v4.6.2 — CI hotfix: land the demo app on the Play Store + API docs (2026-05-16)¶
Fixed¶
- Play Store release deploy no longer blocked by AAB validation (#1416). The pre-upload guard now introspects Android App Bundles with
bundletool dump manifest(the correct tool for.aabfiles) instead ofaapt2, which can only read APKs and was mis-reporting bundles as corrupt. The validation step is also markedcontinue-on-errorso a tooling gap can never veto a release. This unblocks the demo-app Play Store deploy that missed v4.6.0 and v4.6.1. - API-docs deploy no longer races the website deploy on a release tag (#1417).
release.yml's Dokka deploy anddocs.yml's site deploy both push to the externalsceneview.github.iorepo; on a release tag they ran concurrently and the second push failed non-fast-forward. A shared cross-workflowconcurrencygroup now serialises the two pushes so a release reliably publishes both the API docs and the site.
v4.6.1 — CI hotfix: unblock the Play Store deploy for the demo app (2026-05-16)¶
Fixed¶
- Play Store deploy no longer blocked by AAB manifest validation (#1413).
validate-release-artifact.shnow resolvesaapt2from$ANDROID_SDK_ROOT/build-tools/<newest>/instead of relying onPATH(where it never is), and the pre-upload guard now warns and skips instead of hard-failing when the validation tooling itself is unavailable — only a genuine manifest mismatch blocks a release.
v4.6.0 — Demo polish & cross-platform parity: iOS/Android demo unification + Samples tab fixes + AR screenshot regression pipeline + CI hygiene (2026-05-16)¶
Added¶
- Reusable branch + worktree cleanup task. New
.claude/scripts/cleanup-branches-worktrees.shdeletes merged local and remoteclaude/*branches (singlegit push --delete, no bot-burst) and prunes stale.claude/worktrees/*directories, with current-branch / unmerged / open-PR safety guards and a--dry-rundefault. A dailybranch-cleanupjob inmaintenance.ymlprunes merged remote branches automatically. - AR demo screenshot regression pipeline (#1050). New
ARPlaybackScreenshotTestreplays the bundled ARCore recording throughARRecordPlaybackDemoand captures the rendered AR frame at fixed ARCore frame indices (f=30/60/120/180) for golden comparison. Captures are gated on a per-frame counter (DemoSettings.arPlaybackFrameCount, bumped once peronSessionUpdated) rather than wall-clock sleeps, so they land on the same frame on every machine regardless of emulator load. Wired intorender-tests.ymlon a pinned emulator profile and documented insamples/android-demo/AR_TESTING.md. - Flutter & React Native demos: Double Pendulum physics demo (#1332) — a new "Physics" tab in
samples/flutter-demoandsamples/react-native-demoruns the chaotic two-link pendulum with link-length / gravity sliders + reset, mirroring the Android, iOS and web demos. The bridge sample apps have no per-frame transform-mutation API, so the integrator is a 1:1 port of the sharedsceneview-coreDoublePendulumsimulation rendered via a FlutterCustomPainter/ React Native views.Closes #1332.
Changed¶
play-store.ymlnow validates the release AAB manifest before upload (#1301). A new gate runs after thebundleReleasebuild and fails the job fast if the artifact'spackage,versionName, orversionCodedon't match what gradle was told to build — catching a stale or wrong-variant bundle in ~1 s instead of as a Play Console rejection minutes later. Backed by.claude/scripts/validate-release-artifact.sh+ anandroid_cli_describehelper (wrappingaapt2, since theandroidCLI'sdescribesubcommand introspects projects, not built artifacts).Closes #1301.cross-platform-check.shcan cross-check the demo APK manifest (#1302). A new opt-in--with-apkflag builds (or reuses) theandroid-demodebug APK and inspects its manifest via theaapt2-backedandroid_cli_describehelper to verify the exposed entry points match expectations — theio.github.sceneview.demopackage id, a launchableMainActivity, and thesceneview://deep-link scheme — then cross-checks the AndroidDemoRegistrydemo count against the iOSSamplesTabinventory so a platform missing a demo surfaces as drift. The fast source-only path stays the default.- CI hygiene cluster (#1360). Consolidated the website deploy to a single path —
docs.ymlnow publishes the complete built site (marketing + MkDocs + web-demo + Dokka API) to the canonical apex reposceneview/sceneview.github.io, and the redundantdeploy-website.yml(which pushed onlywebsite-static/to a competing URL) is removed. Fixed the iOS SPM cache keys inios.ymlandapp-store.ymlto hash the checked-insamples/ios-demo/Package.resolvedinstead of a nested workspace path that does not exist on a clean runner (the key was a constant empty hash, so the cache never invalidated). Raised theCI Gatejob timeout to 35 min so the poll loop's own diagnostic surfaces before a hard runner kill. Linked the permanentlycontinue-on-errordesktop-democompile step to tracked follow-up #1396. - Unified demo titles & subtitles across Android and iOS (#1376). Every demo now shows one canonical, user-facing title and subtitle on both platforms, so the Play Store and App Store apps no longer look like different products.
- Added
jsTestcoverage for thesceneview-webcore logic classes (#1394). New unit tests pin theOrbitCameraControllerorbit/zoom/pan math (spherical-to-Cartesian eye conversion, phi/distance clamping, auto-rotate, damping), theGeometryGLBBuilderGLB container output (header, chunk alignment, accessors,KHR_materials_unlitextension, node transforms), and the auto-center one-shot gate. ThedidCenterContentflag was extracted into a testableAutoCenterGateso the #1357 regression — a 2ndloadModelmust re-run content centering — is now directly covered.Closes #1394. - API docs: KDoc for the
sceneviewmodule geometry, texture and material helpers (#965). Added accurate KDoc to previously undocumented public declarations in thesceneviewAndroid module: the six geometry builders (Cube,Cone,Cylinder,Sphere,Capsule,Torus), the texture helpers (ImageTexture,VideoTexture,TextureSampler2D/TextureSamplerExternal,Texture.use/setBitmap),RenderableManagerextensions,NodeAnimator, and the ubershaderMaterialInstanceparameter setters. Documentation only — no behavior change.arsceneviewis tracked separately.
Fixed¶
- Samples cleanup (#1361). Rewrote
samples/MULTIPLATFORM.mdso the architecture diagram and recipe list match the real tree (*-demo/folders, the 11 actualrecipes/*.mdfiles). Finished the android-demo first-frame loading-scrim rollout to the remaining 13 non-AR demos so cold starts no longer flash a black viewport (AR demos intentionally skip it — they show a live camera feed, not a black Filament viewport). Converted the deadelse -> PlaceholderDemorouter fallback inMainActivity.ktinto a debug-only drift guard that crashes loudly if a newALL_DEMOSentry is added without a matching route, while still degrading gracefully in release builds. - iOS demo cleanup + Android parity (#1373). Renamed the
Scenestab toSamples, aligned the Samples category taxonomy to Android, removed theAuto Rotate/AR Record & Playbackduplicate entries and the dead Explore buttons, fixed theSettingspill overlapping the controls FAB, and corrected several inaccurate demo subtitles and captions. - iOS demo: Samples tab black rectangle (#1392). Tapping a 3D demo in the Samples tab opened it in a
.medium-detent.sheet, which rendered the demo's full-screenSceneView(RealityView) viewport as a black, half-height panel covering the demo-card list and the Settings button. Every available demo now opens in a.fullScreenCover; the partial.sheetis reserved for the lightweightComingSoonScreen, which has no 3D surface. sceneview-webjsTestsuite can now run in CI (#1401). The Karma / ChromeHeadless test bundle threwUncaught ReferenceError: Filament is not definedat load time — the@JsModule("filament")external is mapped to a global that no script injected into the headless page, which failed the entirejsTestrun before any test executed. Akarma.config.d/filament-stub.jsconfig now serves a no-opFilamentglobal before the test bundle, so the pure-logic web tests (camera/config builders,ContentCentering, version pin, WebXR constants) actually run. Also fixed two latent test failures the blocker was hiding:ContentCentering.centeringOffsetreturned a signed-0.0for an already-centred axis (now normalised to0.0), andSceneViewVersionTest's pinned literal was a version behind.sceneview.github.io/docs/and/api/no longer serve the landing page (#925). Thedocs.ymlworkflow now deploys the complete assembled site — marketing landing page (root), MkDocs technical docs (/docs/), the Kotlin/JS web-demo (/web-demo/), and the Dokka API reference (/api/sceneview/) — to the user-facingsceneview/sceneview.github.iorepo. Previouslydocs.ymldeployed to a different Pages host whiledeploy-website.ymlpublished onlywebsite-static/(which carried adocs/meta-refresh redirect stub) tosceneview.github.io, so the MkDocs and Dokka content never reached those routes and GitHub Pages' 404 fallback served the landing page byte-for-byte. The redundantdeploy-website.ymlworkflow and the obsolete redirect stub have been removed;docs.ymlis now the single authoritative site deploy.MaterialLoader/EnvironmentLoaderno longer leak theirCoroutineScopeacross composition disposal (#933). Each loader'sdestroy()cancels itsCoroutineScope, andrememberMaterialLoader/rememberEnvironmentLoaderwiredestroy()toDisposableEffect.onDispose, so an in-flightloadMaterialAsync/loadHDREnvironmentjob can no longer outlive the owning composition and touch a destroyedEngine.EnvironmentLoader.clear()no longer cancels the scope — it now releases environments only, so callingclear()on a still-live loader never leaves it with a dead scope.- AnimationDemo cinematic camera no longer drains the battery while backgrounded (#974). The four scripted camera loops (Hero, Reveal, Vertigo, Tracking) now park on a clean boundary when the app goes to the background and resume from the exact same pose — no teleport back to the initial yaw, unlike the
repeatOnLifecycle-based helper that #936's review had to revert here. A newLifecyclePausingLaunchedEffect/LifecyclePauseGatepair insamples/commonprovides the reusable state-preserving primitive for anywhile(true)loop that wants lifecycle pausing without a state reset.
Tests¶
- JaCoCo coverage delta gate (#973). A committed baseline (
.claude/data/jacoco-baseline.txt) records per-module line coverage, and.claude/scripts/jacoco-delta-check.shfails when a PR drops coverage more than the configurablethreshold_pp(0.5pp default). Wired into theunit-testCI job as an informational, non-blocking step for now — promoting it to a hard gate once it has been green for two consecutive weeks is the#973follow-up.
Docs¶
/issue-batchskill rewritten as a launch-and-go continuous cycle (#1297). The skill now encodes the validated operating mode: a replace-on-completion pipeline of 6-8 lean-clone background agents (shallow sparse clones, ~0.3-0.6 GB vs ~2.3 GB full), fire-and-forgetgh pr merge --auto, disk-gated spawn (refuse < 15 GB), disjoint-module parallelism, autonomous dispatch, and a release checkpoint per iteration.Closes #1297.
v4.5.0 — visionOS immersive-space skybox + fragment changelog system + iOS unlit/reactive-light parity + CI hardening (2026-05-15)¶
Changed¶
- Adopted a towncrier-style fragment changelog (#1337). PRs now drop a small file in
changelog.d/instead of editingCHANGELOG.md's## Unreleasedanchor, so parallel PRs no longer conflict on the changelog..claude/scripts/collate-changelog.sh X.Y.Zcollates the fragments into a new## vX.Y.Zsection at release time.Closes #1337.
Added — iOS¶
- visionOS immersive-space skybox (#1235). A
SceneViewpulled into a fully immersiveImmersiveSpacenow renders itsshowSkyboxHDR environment as a background. The new.immersiveSpace()modifier opts in; the HDR is mapped onto an inverted sphere parented under aWorldComponentroot, sinceRealityViewContent.environment(the windowed iOS / macOS.skybox(_:)path from #1215) is unavailable on visionOS. Windowed / volumetric visionOS scenes are unchanged.
Fixed — iOS¶
- camera auto-framing now scales-to-fit the scene bounds (#1026, #1041). the
SceneViewdefault camera now dollies to a distance that fits the content bounding box in the viewport — accounting for the vertical fov and live aspect ratio — instead of sitting at a fixed pose, so models are no longer rendered too small, too low, clipped, or overflowing across the ios demos.
Added — Documentation¶
- KDoc for the
sceneview-corecollision API (#965). Documented previously-undocumented public declarations in the collision module (Box,Sphere,Plane,Ray,RayHit,Vector3,Quaternion,Capsule,MeshCollider,ChangeId,TransformProvider) plus theEasingcurve set and the cross-platformlogWarninglogger.
Added — Docs¶
- New recipe: iOS visual-polish pipeline (#1218).
docs/recipes/ios-visual-polish.mddocuments how to combine the v4.4.0 HDR-skybox background render, PBR default material, and Apple AR Quick Look hand-off — decoded from @radcli14'stwolinks. The iOS demo'sDynamicSkyDemodeep-night bucket now uses the dramaticSceneEnvironment.nightSkyHDR.
Added — Samples¶
- Web demo: Double Pendulum physics demo (#1221) — a new "Physics" tab in
samples/web-demoruns the chaotic two-link pendulum with link-length / gravity sliders + reset; the integrator mirrors the sharedsceneview-coreDoublePendulumsimulation that drives the Android and iOS demos. Reachable via the#double-pendulumdeep link.
Fixed — iOS true look-around camera (#1236)¶
- iOS
.firstPersonnow rotates the perspective camera in place instead of orbiting the scene root, so switching orbit ↔ firstPerson no longer teleports the camera; newrecentersTargetOnOrbit(_:)modifier +CameraControls.recenterTarget()fix pan→orbit pivot drift.Closes #1236.
Tests¶
- Regression pins for three untested AR rendering fixes from the 2026-05-14 batch (#1120). New JVM tests pin the
environmentalHdrSpecularFilter = truedefault (#1086), the no-double-close hoisted cubemap upload callback (#1091), and the 7@VolatileLightEstimatortoggles (#1095).
Added — CI¶
- Nightly full-CI safety-net workflow (#1324).
nightly-ci.ymlruns the full heavy validation surface (compile + builds + unit tests + render tests + quality gate) againstmainHEAD once a night, reusing the existing workflows viaworkflow_call, so a path-gated-out regression still surfaces within 24h. Not a PR gate.
Fixed¶
-
iOS
FogNode.heightFalloff/heightBasedare now honestly documented as a RealityKit parity gap (#1380). the height gradient was a silent no-op — a uniform translucent sphere cannot vary opacity by world height; the parameter is kept for Android parity but now clearly documents that height-based fog renders identically to exponential fog on iOS (#1373). -
iOS
GeometryNode/ShapeNodeunlit: truenow returns a flatUnlitMaterial(#1359). Theunlit:parameter previously produced a litSimpleMaterialthat still reacted to scene lighting, contradicting the KDoc contract — it now yields anUnlitMaterial, matchingImageNodeandGeometryMaterial.unlit. -
SceneViewmain/fill light mutations are now reactive (#1306).rememberMainLightNode/rememberFillLightNodere-run theirapplyblock on every recomposition (viaSideEffect), so Compose-state-driven light properties (intensity, direction, color) propagate to the Filament scene without re-keying theremember— matching the iOSRealityView.update:reactive light contract.
Changed — Samples¶
- Migrated the remaining
samples/android-demodemos to therememberMaterialInstance/rememberUnlitMaterialInstancehelpers (#971).CollisionDemo,LightingDemo,VideoDemo,ARStreetscapeDemo,GeometryDemo,DebugOverlayDemo,PhysicsDemoand the sharedAxes3DNodeno longer allocateMaterialInstancehandles via rawmaterialLoader.create*without disposal — the helpers own the lifecycle. Behaviour-preserving.
Fixed — Web¶
sceneview-webstaleSCENEVIEW_VERSION+ auto-center not resetting on a 2nd model load (#1357). The@JsExport-reachableSCENEVIEW_VERSIONwas two majors stale (3.6.0); it now reports4.4.0and is pinned by a jsTest.SceneView.loadModelnow resetsdidCenterContentso a model loaded after the first one was auto-centered gets re-centered, mirroring Android'sSceneAutoCenterState.reset().
Fixed — CI security¶
discord-notify.ymlno longer interpolates user-controlledgithub.event.*fields into inline shell scripts (#1313). Issue title/author and release name/tag now pass throughenv:and are referenced as quoted shell variables, closing a GitHub Actions script-injection vector.
Fixed — Android demo¶
- Demo viewports no longer flash black for 5–12 s on cold start (#1022).
DemoScaffoldnow shows a surface-tinted loading scrim over the 3D viewport until the SceneView presents its first Filament frame, wired via the newrememberFirstFrameState()helper.
Fixed — ViewNode rendering¶
ViewNodeno longer renders as a permanent black rectangle after a background → foreground cycle (#984).ViewNode.WindowManagernow retries the off-screen window attach via an owner-View attach listener when the owner is not yet attached at resume time, instead of silently dropping the attach.
Fixed — Samples¶
SecondaryCameraDemocamera-angle controls are now TalkBack-friendly (#1256). The section label is exposed as a heading and the selectedFilterChipcarries an explicit "Selected camera angle" state description.
Changed — CI¶
-
ci.yml's "Build & lint" job split into parallelbuild/lint/unit-testjobs, andquality-gate.ymlswitched to a shallow checkout (#1311). The three Android jobs share the Gradle cache and run concurrently (~3 min wall-clock saved); the quality gate dropsfetch-depth: 0since its scripts only diff against the working tree / HEAD. -
CI/publish workflows' inline
pip installdeps moved into per-workflow.github/workflows/requirements/*.txtfiles so Dependabot'spipecosystem tracks and bumps them (#1286). Same packages, same pinned versions installed — Dependabot just cannot see inlinepip install x==ylines in workflow YAML, so the pins would have gone stale silently. -
render-tests.ymlreverted from a 3-shard emulator matrix back to a single job (#1119). All 5 render-test classes are class-level@Ignore'd on SwiftShader CI (#803), so the shard matrix booted 3 emulators to run 0 tests — strictly more CI cost for the same coverage. The matrix scaffold can be re-applied once #803 lifts the ignores.
Fixed — arsceneview¶
ARRecorder.start(recordingResolution=…)now restores the session's camera config onstop()(#1358). The higher-resolution CPU image stream raised for a recording no longer silently persists for the rest of the AR session — the prior config is captured before the swap and restored onstop()(and on a failedstart()).
Fixed — Docs¶
- Reconciled stale version refs that survived the v4.4.0 release and hardened
sync-versions.shto catch them (#1356). README badges/CDN/SPM snippets,ai-context.md,android-xr-emulator.md,website-static/js/package.json, the Kotlin toolchain version inllms.txt, and the root↔docsllms.txtARRecorder.saveToPhotoLibraryparagraph now all read 4.4.0 / 2.3.21;sync-versions.shgained checks for every one of those off-map locations.
v4.4.0 — iOS skybox renders + true-orbit camera + iOS Stage 2 demo parity + Double Pendulum physics demo + sceneview-swift mirror retired (2026-05-15)¶
Changed — AR LightEstimator allocation & robustness refactor (#1105)¶
LightEstimator.update()no longer allocates on the AR render thread. Per-frameEstimation, color-correction, cubemap face-offset, RGB-triplet, and 27-element irradiance buffers are now hoisted, reused fields; anENVIRONMENTAL_HDRcapability probe (Session.isSupported, cached per mode) early-returns instead of feeding silently-degraded HDR estimates to Filament; the legacy Sceneform1.8pixel-intensity gain is now a named constant. Public behaviour is unchanged.Closes #1105.
Fixed — AR recording resolution (#1065)¶
ARRecorderno longer records at ARCore's low-res 640×480 default. ARCore writes the CPU image stream into the MP4, whose stock default is the device's lowest-resolution camera config.ARSceneView'ssessionCameraConfignow defaults to the newhighestResolutionCameraConfigselector (highest-resolution BACK-facing, 30 FPS config), so every AR scene — and every recording — runs at full camera resolution without opt-in.ARRecorder.start(...)also gains an optionalrecordingResolution: Size?parameter to request a specific resolution explicitly.Closes #1065.
Added — Agent skills¶
- Published
sceneview-iosandsceneview-webagent skills, and documented the Androidsceneviewskill'sandroid-cliregistry submission (#1080, #1081, #1082). Newagents/sceneview-ios/(SwiftUI + RealityKit) andagents/sceneview-web/(Filament.js + WebXR) skills with install scripts;check-sceneview-skill.shnow validates all three; submission packet and steps tracked inagents/REGISTRY.md.
Added — Web auto-center content (#1052)¶
sceneview-webnow auto-centres loaded content on the orbit-camera target — library-level port of iOSautoCenterContent(#1026). Enabled by default; opt out withautoCenterContent(false)in the DSL builder orsetAutoCenterContent(false)on the JS viewer. Models placed off-origin now frame centred in the canvas without per-demo workarounds. Android sibling tracked in #1051.
Fixed — iOS demo bug cluster (#1054–#1059)¶
- iOS demo bug cluster —
swift testbuild,CameraControlsrename, deep-link, Plane, first-paint audit. Localcd SceneViewSwift && swift testruns the full 603-test suite again (#1054): the 36SceneViewSwiftTestsclasses are now@MainActor-annotated so their RealityKit@MainActornode factories no longer raise ~500#ActorIsolatedCallcompile errors under the Xcode 26 toolchain.OrbitCameraDemo.swiftrenamed toCameraControlsDemo.swift(#1055) so the file matches itsstruct CameraControlsDemo(project.pbxprojsynced). Thesceneview://demo/multi-modeldeep-link no longer hangs on an eternal black "Loading park scene…" scrim (#1056) —MultiModelDemoloads its four park models concurrently and reveals each progressively, so one slow heavy USDZ no longer blocks the rest.GeometryDemo'sPlaneis no longer invisible/edge-on (#1058) — the demo now stands the XZ plane upright to face the orbit camera. First-paint audit (#1059): RealityKit does not exhibit Android #1022's Filament shader-compile black first frame on non-AR demos, so no library-level scrim was needed on iOS.
Added — iOS¶
ARRecorder.saveToPhotoLibrary(_:)now returns the saved asset'sPHAsset.localIdentifier(String?) (#1057). Callers get a handle to the saved recording — resolve it later viaPHAsset.fetchAssets(withLocalIdentifiers:options:)or deep-link to it — closing the cross-platform parity gap with Android'sARRecorder.exportToDownloads()Uri?return. The result is@discardableResult, so existing v4.3.0 call sites are unaffected.
Added — Bridges¶
- Flutter + React Native bridges now expose the v4.3.0 iOS additions —
CameraControlMode(orbit/pan/firstPerson),autoCenterContent, andARRecorder(record-only via ReplayKit) (#1053). Cross-platform consumers can drive iOS camera modes, opt out of auto-centring, and record AR sessions; Android gracefully falls back where the feature is iOS-first (tracked in #1051).
Removed — Samples¶
- Removed the French localization from the sample apps — sample apps are English-only by design (#1294). Deleted
samples/android-demo/.../res/values-fr/strings.xml; the default English resources remain the single source of truth.
Fixed — 3D rendering quality umbrella (#1074)¶
DefaultCameraNodenow starts at a framed 3/4 view — the default 3D camera placement moved from(0, 0, 1)(1 m dead-ahead of the world origin, flat front-on, under-framing anything but a tiny object) to(0, 0.3, 2)looking at the origin. This frames a typical 0.3–1 m model placed at origin correctly out of the box and mirrors iOS RealityKit'slook(at: .zero, from: [0, 0.3, 2])default, so the same scene frames identically on Android and iOS. The position is exposed asDefaultCameraNode.DEFAULT_Y/DEFAULT_Zand pinned inSceneFactoriesTest. This is a visual behavior change — demos that supply their owncameraNodeare unaffected. The remaining umbrella items (IBL intensity 30k→10k, PostProcessingDemo SSAO toggle, light/material leaks, render-quality clobber,indirectLightApplythreading, PhysicsDemo light retune) already shipped in #1079, #1088, #1089, #1092 and #1147.
Changed — CI¶
release.ymlnow deploys the generated Dokka API docs tosceneview.github.io/api/sceneview/<version>/+/latest/and wraps Dokka generation in a 3× retry to tolerate transient Maven Central 503s (#1252, #1127).Deploy iOS App to App Storeis now tag-only — fixes Apple upload-limit failures on every main merge (#1318).- Lighter
mainCI:Deploy Demo to Play Storeis now tag-only (#1321),Deploy website + docsis path-gated to docs/website/markdown changes, andRender Tests/Build sample APKspath filters were tightened to skip doc-only, CI-only andmcp/-only merges (#1311).
Changed — MCP¶
sceneview-mcpaddssearch_android_docs/fetch_android_doctools wrapping Google'sandroid docsCLI, and makes thepackage-filesregression test deterministic by routing thegenerate-llms-txtbanner to stderr (#1083, #1113).
Added — Double Pendulum demo (shared KMP physics)¶
- New
DoublePendulumsimulation insceneview-core(Addresses #1221) — a pure-Kotlin, platform-independent two-link (double) pendulum inio.github.sceneview.physics.DoublePendulumStateholds twoDoublePendulumLinks (length, mass, angle, angular velocity), a fixedpivot,gravityanddamping;DoublePendulum.step(state, dt)advances it with a symplectic (semi-implicit) Euler integrator, sub-stepped at1/240 sso the chaotic motion stays numerically stable at any frame rate. Exposesjoint/tipjoint positions and atotalEnergyaccessor; covered by 12commonTestcases including energy-conservation (bounded energy band withdamping = 0) and rest-state stability. Adapted from @radcli14's MIT-licensedtwolinks. - New "Double Pendulum" demo on Android and iOS — a chaotic two-link mechanism rendered as metallic PBR links swinging in real time, driven by the shared
DoublePendulum. Android (samples/android-demo) wires thesceneview-coresimulation into aSceneView { }frame loop with sliders for link lengths / gravity / reset; iOS (samples/ios-demo) ships the SwiftUI equivalent. The iOS demo hand-ports the same integrator math into a localDoublePendulum.swift, kept numerically identical to the Kotlin source, since iOS cannot consume the KMP module directly until thesceneview-coreXCFramework lands (#1033). Reachable viasceneview://demo/double-pendulumon both platforms. Web / Flutter / React Native ports are deferred follow-ups under #1221.
Changed — iOS default material is now physically-based (#1223)¶
- Procedural geometry on iOS now defaults to
PhysicallyBasedMaterialinstead ofSimpleMaterial(#1223) —GeometryNode.cube/sphere/cylinder/cone/plane/torus/capsule(color:),ShapeNode(points:color:),LineNode(from:to:color:), and litImageNodes previously created RealityKitSimpleMaterial, which is effectively unlit-flat: it does not react to image-based lighting (the HDR environmentSceneViewwires by default) and cannot express metallic/roughness. The library default is now a matte-neutral PBR material (metallic: 0,roughness: 0.5) so shapes pick up soft environmental shading and reflections — the single biggest visual-quality jump for iOS demos. This is a visual behavior change, not a breaking API change: every existing call site keeps compiling and produces visually-similar-or-better output. Callers who explicitly want the old flat-fill look (debug visualizations, overlays) can opt back in with the newunlit: Bool = falseparameter, e.g.GeometryNode.cube(color: .red, unlit: true). Also fixes a latent bug whereGeometryMaterial.pbr(...)was internally backed bySimpleMaterial(so metallic/roughness never reached the PBR pipeline) — it now correctly builds aPhysicallyBasedMaterial.Closes #1223.
Added — night_sky environment preset (#1219)¶
- New
night_skyHDR environment bundled across iOS and Android demos — a dramatic Milky Way starfield over a dark landscape (Poly Havendikhololo_nightby Greg Zaal, CC0 1.0 public domain). iOS exposes it asSceneEnvironment.nightSky(added toallPresets, so it auto-surfaces in the demo's environment picker); Android adds a "Night Sky" chip toEnvironmentDemo. Pairs well with metallic PBR materials for chrome-mirror reflections. Web demo does not bundle HDRs and is unaffected.
Changed — iOS demo¶
- The AR tab launcher now doubles as a discovery surface (#1253) — a 2×3 grid of headline AR demo cards under the "Start AR Camera" CTA (Plane Placement, Instant Placement, AR Lighting, AR Recording, Orbital AR, AR Debug), each opening the demo full-screen — closing the launcher-parity gap with Android's
ArLauncherScreen. The AR tab's placed-model count is also derived from the live anchor collection so it can no longer drift.
Fixed — iOS demo¶
AppStoreUpdatersnooze is now version-keyed instead of a 7-day TTL (#1231) — dismissing one release's update banner no longer hides a newer release's banner; a new App Store version invalidates the snooze automatically, matching the Web/Flutter/RN samples. TheSceneViewDemoTestsunit-test target is now wired intoSceneViewDemo.xcodeprojso theAppStoreUpdatertests run in CI (#1227).
Fixed — iOS rendering¶
-
SceneEnvironment.showSkybox = truenow actually paints the HDR as the scene background (PR #1215, ported from @radcli14's sceneview-swift#1) —SceneViewpreviously loaded the HDR and applied it as IBL viaImageBasedLightComponent, but never assigned it toRealityViewContent.environment, so the scene rendered against the default neutral void regardless of which environment preset was selected. The new path caches the loadedEnvironmentResourcein a@Stateand applies it viacontent.environment = .skybox(resource)in theRealityView.update:closure with a diff guard against the last applied resource (no per-frame ARC churn). The.task(id:)keys on(name, showSkybox)so toggling the flag on the same env re-runs the loader, and clears the cached resource at the start of every task tick so cross-env transitions don't show stale skyboxes under a new IBL. -
Orbit + pan camera modes now physically move the perspective camera in world-space (PR #1215) —
applyCamera()was faking the camera move by rotating + scalingentities.rootwhile the perspective camera stayed pinned at[0, 0.3, 2]. With a global skybox, that made the background appear stationary while content visually orbited around the user — visually wrong from the camera's POV. Orbit and pan now position the camera viaCameraControls.cameraPosition()+look(at: target, ...), so the skybox correctly wraps. The scene root stays at identity for both modes;camera.orbitRadiusis now the literal camera-to-target distance.firstPersonretains its rotate-the-root semantics (FOV pinch via #1034) — the true "stand still and look around" rewrite remains a v4.4.0 follow-up. -
FOV no longer bleeds from
firstPersonpinch intoorbit/pan— switching tofirstPerson, pinching FOV down to e.g. 30°, then back toorbitkept the 30° pinched FOV on the perspective camera (visible as a stuck zoom-in).applyCamera()now writes the baseline60°FOV inorbit/panregardless ofcamera.fov, and only mirrorscamera.fovinfirstPerson. OnfirstPersonexit,camera.fovitself is reset to60so the next entry starts fresh.
Changed — CameraControls defaults (BREAKING for direct constructors)¶
CameraControls.orbitRadiuspublic default changed from5.0to2.0—5.0was unreachable through any public modifier (cameraControls(_:)only accepts aCameraControlMode), and the internal@Statealready overrode to2.0so existing demos retain their on-screen framing. Callers constructingCameraControls()directly will see the same2.0default the SceneView uses internally; the apparent angular size of a 1m model at default state is identical to the pre-v4.4.0 fake-orbit framing (28.07° at 60° FOV).CameraControls.minRadiuspublic default changed from0.5to1.0— under the new true-camera path,0.5puts the perspective camera inside any model with extent >1m (which most demo content has). The old0.5was safe under the fake-orbitscale = 5.0 / radiusscene-scale hack but clips into geometry now. Override for smaller content.
Changed — SPM URL retirement¶
sceneview-swiftSPM mirror retired in favour of monorepo-direct package resolution (PR #1215) — every install snippet across docs, codelabs, GPT prompts,.github/copilot-instructions.md,SceneViewSwift/README.md,llms.txt(4 copies — root, docs, website-static, well-known), the website (index.html/docs.html/playground.html), the PWA manifest, the schema.orgsameAsgraph, and the bundled MCPllms-txt.tsnow points athttps://github.com/sceneview/sceneview(.git). Thesceneview/sceneview-swiftmirror has been archived read-only; its frozenv4.0.0tag still resolves for SPM consumers pinned to the old URL, but no further releases will be cut there. Existing consumers should re-add the package in Xcode pointing at the monorepo URL — the rootPackage.swift(added in PR #920) declares theSceneViewSwiftproduct.
Changed — CI / scripts hardening¶
- CI/scripts hardening batch (#1226, #1230, #1237, #1114) — new
check-sceneview-swift-urls.shPR gate forbids reintroducing the archivedsceneview-swiftmirror URL;sync-versions.shnow uses a portable_sed_inplacehelper (BSD/GNU); publish-timepip installcalls inplay-store.yml/app-store.ymlare pinned to exact versions. CONTRIBUTING.md now documents that docs-only PRs skip the quality-gate + render-tests (#1128).
Fixed — in-app update polish across all 6 sample platforms (#1244–#1249)¶
Follow-ups to PR #1216 (in-app auto-update feature) — six small, platform-specific hardening fixes that bring the auto-update behaviour to parity across Android, Web, React Native, Flutter, iOS and macOS:
- Android —
InAppUpdateManagerlistener stacking (#1244): the early-return guard added in #1216 only coveredDOWNLOADING/READY_TO_INSTALL. A fast double-resume landing twocheckForUpdate()calls while the first was still in theCHECKING/AVAILABLEwindow would issue a parallelappUpdateInforequest and a duplicatestartUpdateFlow, double-prompting the user. A privateinFlightflag now gates re-entry, set on entry and cleared on both the success and failure listener so a network failure can't permanently lock out checks. Covered by two new Robolectric tests inInAppUpdateManagerTest. - Web — update snackbar hidden behind the loading overlay (#1245): the snackbar (
z-index: 60) sits below the loading overlay (z-index: 100), so a version check resolving during engine init showed the snackbar stranded behind the spinner. The snackbar is now gated on engine-init complete — if the check resolves early it is deferred and flushed onceloading-overlay.hiddenis set. - React Native — update banner overlapping the header (#1246): the absolutely-positioned banner used
top: 0, overlapping the "SceneView / React Native Demo" header. It now offsets by aHEADER_HEIGHTconstant so it sits cleanly below the header (react-native-safe-area-contextis not a dependency of this demo, so a measured constant is the minimal fix). - Flutter — deprecated
withOpacity()(#1247):Color.withOpacity(0.8)inapp.dartreplaced with the Flutter 3.27+withValues(alpha: 0.8)API.flutter analyze lib/app.dartnow reports no issues. - iOS / macOS —
AppStoreUpdater.openAppStore()no-op on macOS (#1248): theopencall was wrapped in#if canImport(UIKit) && os(iOS), making it a silent no-op on the macOS target. A#elseif os(macOS)branch now opens the Mac App Store viaNSWorkspace.shared.openwith themacappstore://scheme. - iOS — update throttle hard-locked on clock rollback (#1249): if the system clock rolled backward,
now − lastCheckAtwent negative — always below the throttle — so updates never re-checked.shouldCheck()now clampslastCheckAttomin(last, now)on read, repairing a future-stamped timestamp. Covered by a new test inAppStoreUpdaterTests.
Fixed — Android demo regressions (#1265, #1266)¶
Two regressions from cd4034ff (PR #1224) that the #1241 emulator QA sweep proved still broken:
- AR Instant Placement — "Initializing camera" pill alignment (#1265): the scanning-indicator pill rendered bottom-left, overlapping the Clear All button, despite a
BoxScope.align(TopCenter)set directly on itsAnimatedVisibilitywrapper.AnimatedVisibilityintroduces its own layout node for the enter/exit transition and thealignmodifier on that wrapper is not reliably honoured by the animated child. The alignment is now carried by a staticBoxthat is a direct child of the outerBox, withAnimatedVisibilityinside it handling only the fade — matching the structure of the working stats pill above. Pill stays pinned top-center, clear of the bottom Clear All button. - Debug Overlay — invisible stress-test spheres (#1266): the
SceneViewcontent block had noLightNodeand the spheres were spawned with nomaterialInstance, so the default Filament material rendered black on the black background. Added a directional key light and a shared on-brand color material (created once viaremember, so the stress test still measures pure geometry overhead). The earlier #1212 grid-centering fix already places the count == 1 sphere at origin; this completes the visibility fix.
Fixed — tooling¶
- Web demo — deferred update snackbar stranded on engine-init failure (#1279) —
flushPendingUpdateSnackbar()(added in PR #1271 to defer the update snackbar past engine init) was only called in theSceneView.modelViewer(...)success path; an engine-init rejection left a deferred version stuck inpendingUpdateVersionforever. The.catch()path now flushes too — the snackbar is pure DOM andflushPendingUpdateSnackbar()nullspendingUpdateVersion, so it can never double-show.Closes #1279. -
DemoInteractionTest— FR-locale gap in control helpers (#1282) —secondaryCamera_pipAnglesnow resolves the PiP-angle chip labels fromR.string.demo_secondary_camera_chip_*instead of hard-coded English literals, so the interaction test passes on a French-locale device. Demos that still inline English control labels in the composable need a per-demo resource-extraction sweep first (tracked separately).Closes #1282. -
worktree-auto-prune.shno longer risks destroying a parallel session's uncommitted work (#1278) — the script now skips any worktree with a non-emptygit status --porcelain, uses plaingit worktree remove(fail-safe) instead of--force, accepts repeatable--keeppaths, and reclaims squash-merged worktrees via agh-backed merged-PR check that degrades gracefully offline.Closes #1278.
Docs¶
- New recipe: Blender → SceneView asset pipeline (#1222) —
samples/recipes/blender-to-sceneview.mdanddocs/docs/recipes/blender-pipeline.mdwalk contributors through authoring a custom 3D model in Blender and shipping it in a SceneView app:.glbis native on Android, while Apple platforms go.glb→ Reality Converter →.usdz→ Reality Composer Pro (Blender's own USDZ exporter produces broken materials). Adapted from @radcli14'sblender-to-realitykittutorial (MIT, 17⭐), with a SceneView-specific call-out on the Android Filament JNI main-thread rule. Cross-linked from both quickstarts and the API cheatsheet. Closes #1222.
Added — Android library-level autoCenterContent (#1051)¶
SceneView(autoCenterContent = true)— port of the iOSautoCenterContentfeature (#1026 / PR #1038). DSLcontentnodes are parented to an intermediate content-root node which the library translates once — on the first frame their union bounding box is non-empty — so the content centroid lands at the orbit pivot and renders centred without per-nodeModelNode(centerOrigin = …). Lights / camera areSceneViewparameters (never DSL children) so they stay put. Opt out withautoCenterContent = falsefor intentional off-centre composition.
Follow-ups (filed against the master polish-pipeline reference #1218)¶
- #1219 — Bundle ambientCG NightSkyHDRI008 (CC0) as
night_skyenv preset (iOS + Android + Web) - #1221 — Cross-platform 'Double Pendulum' physics demo (port of @radcli14's
twolinks) - #1222 — Recipe: Blender → glb → Reality Converter → usdz → Reality Composer Pro pipeline
- #1223 — Switch library-default material from
SimpleMaterialtoPhysicallyBasedMaterial
Special thanks to Eliott Radcliffe (@radcli14) — the skybox + true-orbit camera fixes were ported with Co-authored-by credit from his sceneview-swift PR #1. The asset-pipeline tutorial referenced by #1222 is from his blender-to-realitykit repo (MIT, 17⭐).
v4.3.6 docs hotfix — Cloud Anchor ERROR_NOT_AUTHORIZED post-SHA-1 troubleshooting (#1177 follow-up)¶
iOS Stage 2 demo parity catch-up (#1194)¶
Six Android-only Sketchfab-streaming demos shipped by Stage 2 (#1152) now have proper iOS ports so the cross-platform parity guarantee (feedback_ios_mirror_android.md: iOS V1 == strict Android subset, no hidden gaps) holds end-to-end. The previous placeholder shape — model-viewer / multi-model deep-links routing to SceneGalleryDemo — is gone.
Added — iOS samples¶
AnimationDemo.swift— 5-model carousel (bundled cyberpunk character + 4animation-category streamed slugs) with play / pause / speed slider / loop chips. Cinematic camera shots (Hero / Reveal / Vertigo / Tracking) + IBL intensity slider from Android remain Android-only — see the iOS demo's settings sheet for the upfront roadmap note.ModelViewerDemo.swift— full-screencyberpunk_hovercarhero with a "Surprise me" extended button that searches the Sketchfab catalogue server-side, downloads the pick viaSketchfabService.downloadModel, and replaces the hero in place. Button hidden whenSketchfabConfig.apiKeyisnil(App Store builds) so we don't ship a non-functional affordance.MultiModelDemo.swift— themed "Park" diorama (tree / bench / dog / bird) composed from the 4 streamedpark-category slugs. Per-model visibility chips + spin toggle wired throughAnchorEntity+SceneView.autoRotate(speed:).ARPlacementDemo.swift— tap-to-place AR demo with a 5-bundle cycle and the 6 streamedar_placement-category chips. ReusesSceneViewSwift'sARSceneView(onTapOnPlane:)raycast hook.ARInstantPlacementDemo.swift— instant-placement variant with a toggle. ARKit doesn't exposeConfig.InstantPlacementMode.LOCAL_Y_UPdirectly; the iOS port approximates via.estimatedPlaneraycasts so taps land before plane geometry has fully converged.PhysicsDemo.swift— rewritten from the v4.3.x cubes-only version to the Stage 2 streaming shape: bundled cubes default + 4 streamedphysics-category crash-test meshes (vase / stool / barrel / amphora). Drop count capped at 20 active bodies because RealityKit'sPhysicsBodyComponentslows past that.
Changed — iOS plumbing¶
AutoRotateDemo.swiftstruct renamed fromAnimationDemo→AutoRotateDemoto free up the canonical name. The "Auto Rotate" Samples-tab entry continues to point at this struct; the new "Animation" entry routes toAnimationDemo.swift.SamplesTab.swift— added Model Viewer / Multi-Model Park entries under Geometry, and promoted "AR Plane Placement" + "AR Instant Placement" fromComing soonto fully wired demos.DemoDeepLinkRegistry.swift—model-viewerandmulti-modelids no longer route to theSceneGalleryDemoplaceholder; both land on the dedicated demos.ar-placementnewly routed toARPlacementDemo.
Fixed — iOS Stage 2 demo polish (#1280)¶
ARPlacementDemo/ARInstantPlacementDemogain a "Clear all placed models" control that tears down every placed anchor (placed anchors previously accumulated for the demo's lifetime);ARInstantPlacementDemo's Instant/Plane toggle doc-comment + copy now honestly state both modes use the same.estimatedPlaneraycast (the toggle only shows/hides the plane + coaching overlays);ModelViewerDemo's "Surprise me" failures now surface a transient error banner instead of failing silently; and a confusing double-negation inMultiModelDemowas simplified.
Fixed — pre-existing AppStoreUpdater build break¶
AppStoreUpdater.swift:66default parametercurrentVersion: @escaping () -> String? = AppStoreUpdater.bundleVersionwas losing the@MainActorglobal-actor isolation under Swift 6 strict concurrency, breaking the iOS demo build on main. Added@MainActoron both the parameter type and the stored field so the implicit@MainActorfrom the class scope propagates correctly. Surfaced while validating #1194; the regression landed in #1216 earlier today.
Docs¶
docs/docs/cheatsheet-ios.md— new "Demo parity status (#1194)" section above the existing "iOS parity status (#1036)" table, summarising the six ports and the honest-subset notes (cinematic camera, per-model editing, sceneview-core physics).
No library APIs change. No new releases of :sceneview / :arsceneview / :sceneview-core are required.
Production Cloud Anchor users still hitting ERROR_NOT_AUTHORIZED on v4.3.5 after the App Signing key SHA-1 was added to the Google Cloud API key restrictions. v4.3.3 (PR #1197) shipped the SHA-1 runbook + actionable in-app error pointing only at that one cause, but field experience showed there are 4 other Cloud-Console-side causes that look identical at the device.
Investigation confirmed every code-side surface is healthy:
- ARCORE_API_KEY GitHub secret present (39 chars, last rotated 2026-05-06)
- samples/android-demo/build.gradle injects manifestPlaceholders["arcoreApiKey"] from env / local.properties
- AndroidManifest.xml carries <meta-data android:name="com.google.android.ar.API_KEY" android:value="${arcoreApiKey}" />
- ARCloudAnchorDemo.kt enables Config.CloudAnchorMode.ENABLED in sessionConfiguration
- play-store.yml's verify-arcore-key.sh CI guard passed green on the v4.3.5 release run (run 25891143675, 2026-05-14 23:24 UTC)
- Package name io.github.sceneview.demo matches the Cloud Console restriction (no applicationIdSuffix)
So the bug is Cloud-Console-side configuration drift, not an APK-side regression. v4.3.6 expands the docs surface so the next maintainer / contributor hitting this can self-diagnose without escalating.
Changed¶
samples/android-demo/STREETSCAPE_SETUP.mdadds a new "Troubleshooting —ERROR_NOT_AUTHORIZEDpersists after SHA-1 is whitelisted" subsection under the existing "Play App Signing key" block. Five-step checklist with direct Cloud Console deep-links (replace<PROJECT_ID>withpc-api-4638313286439917620-648for the SceneView demo project):- Billing enabled and active on the Cloud project (Geospatial / Cloud Anchors hit paid backends; silently rejects without billing).
- "ARCore API" enabled (not the legacy "ARCore Cloud Anchor API" — different products).
- API restrictions on the key separate from Application restrictions — must include "ARCore API" by name, or be set to "Don't restrict key".
- Propagation delay — observed up to 30 min in practice despite Google's "~1 min" claim.
-
Project-ID mismatch — verify the API key whose SHA-1 you whitelisted is the same key in the GitHub secret.
-
ARCloudAnchorDemo.kthost/resolve error messages broadened. The in-app banner forERROR_NOT_AUTHORIZEDno longer presumes the SHA-1 is the cause — it now reads "Check SHA-1 + billing + ARCore API restrictions in STREETSCAPE_SETUP.md.". This matches the v4.3.3 hotfix's actionable-error spirit but covers the full failure mode space surfaced post-#1177. -
.claude/scripts/verify-arcore-key.shreminder footer broadened to direct maintainers reading the CI log at the new 5-step checklist rather than only the SHA-1 runbook.
Fixed — android-demo¶
- Secondary Camera demo — restore PiP overlay (PR #1213) —
SecondaryCameraDemo.ktwas renamed "Camera Presets" in commitdfc241d5and lost its picture-in-picture overlay; the chips ended up just snapping the main camera, defeating the "multi-camera" pitch even though the registry entry still ships thePictureInPictureicon + "Picture-in-picture camera view" subtitle. TwoSceneViews now share the same engine/loaders and render the helmet simultaneously: the main view keeps the default orbital camera (user-interactive), and a smallSurfaceType.TextureSurfacePiP overlay top-start binds a dedicatedrememberCameraNodedriven by the Top / Side / Front / Corner chips viaLaunchedEffect(cameraPreset). Title restored to "Secondary Camera (PiP)" soDemoInteractionTest.secondaryCamera_pipAnglesfinds it again. Two correctness invariants doc'd inline: eachSceneViewgets its OWNrememberModelInstance(sharing one across views would double-destroymodelInstance.rooton dispose — SIGABRT — and reparent child light/camera nodes off whichever ModelNode built last) and the PiP receivescameraManipulator = null(without it the SceneView frame loop writescameraNode.transform = manipulator.getTransform()every frame, clobbering theLaunchedEffectpreset writes). iOS gets the matching "Coming soon" placeholder under.advanced(SamplesTab.swift,pip.fillSF Symbol, v4.4) —SceneViewSwift currently uses an internal@State private var camera = CameraControls(mode:)with no per-instancecameraNodebinding, so a true RealityKit PiP needs new SceneViewSwift public API (tracked for v4.4).
No library APIs change. No new releases of :sceneview / :arsceneview / :sceneview-core are required — the Cloud Anchor on-device fix is entirely Cloud Console configuration; the Secondary Camera fix is scoped to samples/android-demo/.
Changed — in-app update (samples)¶
InAppUpdateManagerTestnow covers the intermediateDOWNLOADINGstate + non-zerodownloadProgress(#1229);UpdateBannerauto-focuses its "Restart" CTA on D-pad hosts (#1228) — the TV demo passes an optionalrestartFocusRequesterso the Restart button grabs focus when an update reachesREADY_TO_INSTALL; phone hosts leave itnulland are unaffected.
v4.3.5 — Pixel 9 production polish: AR demo UX fixes + FR i18n + CI dedup + iOS pull-to-refresh (2026-05-15)¶
Added — iOS pull-to-refresh on Explore feeds (#1211 item 1 — PR #1225)¶
- iOS pull-to-refresh on Sketchfab Explore feeds —
samples/ios-demo/SceneViewDemo/Views/ExploreTab.swiftnow wires.refreshable { await loadSketchfabFeeds(force: true) }on the ExploreTabScrollView, mirroring the AndroidPullToRefreshBoxshipped in v4.3.4 (PR #1203). NewloadSketchfabFeeds(force: Bool = false)overload bypasses the "already loaded" guard when invoked from the swipe-down gesture, and conditionally gates the loader onSketchfabConfig.apiKeyso builds without the key don't spinner-flash on every refresh. Items 2 (matchedGeometryEffecthero zoom) and 3 (ARTab close affordance) from #1211 remain open as follow-ups.
Fixed — SPM version drift caught post-v4.3.4 (PR #1217)¶
- 2 stale SPM
from: "4.3.3"references bumped to 4.3.4 —pro/gpt-store/gpt-instructions.md:77andmarketing/stackoverflow/qa-drafts.md:215. Both files live in non-canonical directories thatsync-versions.shdoesn't sweep, so the drift slipped past the v4.3.4 release cut (#1153). Surfaced by.claude/scripts/impact-check.shafter PR #1203 landed.
Changed — CI workflow deduplication (~20 min saved per PR)¶
- Workflows trimmed — Audit of
.github/workflows/showedassembleDebugcompiling 4× per PR (across CI, PR Check, quality-gate, Build sample APKs) and unit tests running 3×. Every duplicate removed while keeping every distinct check: pr-check.yml— droppedcompile-android,lint,compile-web-demo,build-flutter-demo(all already covered byci.yml'sbuild,web-desktop,flutter-demojobs). Kept only the unique fast guards:check-deprecated-api,check-sceneview-skill,compile-kmp(KMP all-targets, beyondci.yml's JS-only build),check-workflow-scripts,validate-demo-assets. Also mirrored thepaths-ignoreblock fromci.ymlso docs-only PRs no longer spin up the ~5 min Gradle KMP compile.build-apks.yml— dropped thepull_requesttrigger. APKs were already built twice on every PR byci.yml+pr-check.yml; this workflow's unique value (artifact upload, GitHub Release attachment) only matters onpush/ tag.quality-gate.yml+.claude/scripts/quality-gate.sh— addedQUALITY_GATE_SKIP_ANDROID=1env var, set in the CI workflow so the gate no longer re-runsassembleDebug+ the same Android unit tests thatci.yml'sbuildjob already executes (with JaCoCo coverage). Local invocations ofquality-gate.shstill run the full path. MCP tests, version sync, security scans, asset CDN checks, website rules, and agent skill drift detection all still run on every PR and push.-
render-tests.yml— dropped thepull_requesttrigger. Tests are non-blocking (continue-on-error: true) and produce screenshots rarely consulted by reviewers; the signal is still captured on every push to main, withworkflow_dispatchavailable for ad-hoc feature-branch vetting. -
Supply-chain guard centralised — Moved
gradle/actions/wrapper-validation@v6from the (now removed)pr-check.yml:compile-androidstep into.github/actions/setup-gradle/action.ymlso every workflow that calls./gradlew(CI, PR Check, quality-gate, build-apks, render-tests, release, docs) inherits the validation. Catches any tamperedgradle/wrapper/gradle-wrapper.jarregardless of which workflow consumes it first.
Validated by 4 independent Opus reviewers before merge. Branch protection on main confirmed to have zero required status checks, so no renamed job blocks merges. No downstream workflow_run, needs:, Renovate, Codecov, or contributor doc reference was broken (verified via grep -rn workflow_run .github/workflows/ and a sweep of the last 20 merged PRs for artifact-name references).
Fixed — Pixel 9 v4.3.0 production audit follow-ups (umbrella #1176)¶
Five demo polish bugs caught in the Pixel 9 production audit. All are scoped to samples/android-demo/ and samples/android-demo/src/main/res/values-fr/strings.xml — no library APIs change.
-
AR Instant Placement — "Initializing camera" pill overlapped Clear All at startup (#1199) —
ARInstantPlacementDemo.ktnow hides the bottom-start "Clear All" button until at least one anchor has been placed (dead affordance pre-tap), and moves the "Initializing camera — you can already tap to place" toast pill fromBottomCentertoTopCenter(56 dp below the stats pill). Before, the two competed for the bottom anchor area and the user saw what looked like two half-overlapping buttons. Now: top of screen carries the transient init message, bottom is empty until an anchor exists. -
AR Pose Placement — primitives appeared unlit on Pixel 9 (#1200) —
ARPoseDemo.ktretunes both cube and sphere PBR materials fromroughness=0.85, reflectance=0.1toroughness=0.55, reflectance=0.2. The previous values were pinned all the way to "matte safety" to avoid an IBL specular blowout on the original metallic=0.5 setup, but swung too far the other way under ARCoreENVIRONMENTAL_HDR— the sphere lost all visible diffuse falloff and read as a flat 2D circle next to a barely-shaded cube. The new mid-rough setting keeps the IBL safe (no blowout, metallic stays 0) while restoring the diffuse gradient that makes the sphere read as a 3D sphere. -
Sketchfab model viewer — initial expand rendered model inside a circular crop (#1201) —
SketchfabModelViewerScreen.kt::RenderContentnow defers mountingSceneViewuntilrememberModelInstanceresolves (instance != null). Before, the SceneView was always composed and an opaque-surface loading placeholder was layered on top with a centeredCircularProgressIndicator. During the bottom-sheet expand transition, the opaque surface faded relative to the still-rendering SceneView surface underneath, producing a brief "model visible inside a circular porthole" frame (the user could see the model through the fading surface overlay, with the centered spinner ring framing the visible area). Now the placeholder owns the full 440 dp box cleanly until the GLB is ready, then the SceneView mounts in one swap. -
i18n: missing French translations for streamed-model credits sheet + asset-source chips (#1204) —
values-fr/strings.xmladds the 7 keys flagged by the post-#1099/#1160 audit:credits_sheet_title/credits_sheet_subtitle/credits_sheet_footer/credits_row_open_cdfor the streamed-model attribution sheet, anddemo_chip_bundled/demo_chip_streamed/demo_chip_streamingfor the DemoScaffold asset-source chip. Thedemo_ar_streetscape_*keys called out in the original issue body were already translated; this PR closes the broader audit gap discovered bycomm -23 used_keys.txt fr_keys.txt(139 used keys, 7 had EN entries but no FR entries). -
Debug Overlay — single-sphere case spawned off-screen at (-0.9, -0.9, 0) (#1212) —
DebugOverlayDemo.ktnow computes the grid footprint from the actual node count:cols = min(10, count),rows = min(10, ceil(count / cols)),layers = ceil(count / (cols × rows)), then offsets each sphere by-(axisLen - 1) / 2 × NODE_SPACINGso the cluster mean is always at origin. At count=1 → cols=rows=layers=1 → offsets all zero → sphere lands at (0, 0, 0) where the camera is looking. At count=100..1000 the new formula collapses to the same 10×10×N centered footprint as before. The previous formula(i % 10) - 5baked in a "10 wide" assumption that put count=1 at (-0.9, -0.9, 0) — 3.6× outside the camera frustum at the SINGLE_SPHERE_DISTANCE = 0.8 m camera distance.autoFitDistance(...)updated to read the new grid footprint so framing stays consistent.
v4.3.4 — Pixel 9 production hotfix: AR Face Mesh + Instant Placement UX + UTF-8 + iOS LightingDemo (2026-05-15)¶
Fixed — Sketchfab Explore cosmetic & iOS demo gaps¶
-
Sketchfab Explore — Polish name
My�liniceshows U+FFFD (#1181 — PR #1202) —SketchfabService.authenticatedGetnow decodes the response body as UTF-8 explicitly viaresponse.body.source().readString(Charsets.UTF_8)instead ofbody.string(). OkHttp'sstring()honours theContent-Typecharset and falls back to ISO-8859-1 when the header lacks acharset=parameter (which can happen at edge-cache rewrites), corrupting any non-ASCII byte. Sketchfab's API always returns UTF-8, so forcing the decode is both correct and defensive. New unit testdecodes non-ascii model names without substitutionexercises Polish / Czech / Greek / CJK fixtures. -
AR Examples menu — green pills replaced with M3 Expressive grid (#1185 — PR #1202) —
ArViewTab.kt'sArDemoCardnow mirrors theDemoCardpattern fromDemoListScreen.kt: gradient-tinted icon header on top + title + subtitle below, using the "Augmented Reality" category green accent (light#66BB6A/ dark#A5D6A7) so the AR View launcher feels like the same app as the Samples tab. Pre-refactor the cards used floating tertiary-tinted pills that read as a "different app" against the Samples-tab grid. -
iOS sample —
ARLightingDemo.swiftcompanion to #1151 fillLightNode port (#1155 — PR #1202) — New AR demo atsamples/ios-demo/SceneViewDemo/Views/Demos/ARLightingDemo.swiftshowcases the.mainLight(_:)+.fillLight(_:)modifiers shipped in v4.2.0 (PR #1151). Three filter chips toggle between.systemDefaulton both slots, dim-key.custom(LightNode.directional(intensity: 5_000)), and key-only (.fillLight(.disabled)) — registered under the AR section inSamplesTab.swift.
Added — Compose UX patterns in samples/android-demo¶
- Pull-to-refresh on Explore Sketchfab feeds (
ExploreTabScreen.kt) —PullToRefreshBoxreloads the Trending / Staff Picks / Recently Added carousels on swipe-down. The pull-down affordance is conditionally wired so it only shows when the Sketchfab API key is present (no spinner-flash on builds without the key). The refresh path goes through a single cancel-then-restart pipeline (refreshTickLaunchedEffect key) so toggling the "Animated" filter mid-refresh can't race two concurrent loads writing to the same lists. - System back exits live AR session (
ArViewTab.kt) —BackHandlerroutes the system gesture to the same exit path as the top-end Close button (detach anchors, return to the AR launcher screen). Manifest opts intoandroid:enableOnBackInvokedCallback="true"so Android 13+ routes back via the newOnBackInvokedDispatcher(prerequisite for any futurePredictiveBackHandlerupgrade). - Shared-element hero morph between viewer stages (
SketchfabModelViewerScreen.kt) —Crossfadereplaced withSharedTransitionLayout+AnimatedContent. The 220 dp Preview thumbnail morphs in place into the 440 dp Ken-Burns Downloading hero, then into the live SceneView surface, sharing bounds across the three stages with a consistent rounded-corner clip. The live render usesSurfaceType.TextureSurfaceso the layer alpha is honoured during the morph (the defaultSurfaceViewis a hardware overlay and would pop in opaque). Stage.Error is excluded from the shared bounds (no hero) and uses a clean 300 ms fade.
Added — iOS demo parity (umbrella #1211)¶
.refreshableon Explore Sketchfab feeds (ExploreTab.swift, PR #1225) — pull-to-refresh on the iOSScrollViewmirrors the AndroidPullToRefreshBoxin #1203.loadSketchfabFeeds(force: Bool)bypasses the "already loaded" guard when called from.refreshableso manual pulls actually re-fetch.- iOS 18 zoom navigation transition Explore card → viewer (PR #1232) —
.matchedTransitionSource(id:in:)on the carousel card pairs with.navigationTransition(.zoom(sourceID:in:))on the destination so the thumbnail morphs into the viewer's preview hero on push. The viewer now exposes an explicitStage.Preview(description / tag chips / "Open in SceneView" CTA / non-downloadable warning) matching Android'sStage.PreviewPreviewContent— the network download only fires after the user taps the CTA, and a Retry button on the error overlay resets to the preview state. Source IDs are namespaced by feed ("sketchfab-hero-staff-…"/"-liked-"/"-recent-") so a model appearing in more than one carousel doesn't collide on the matched namespace.
Fixed — Pixel 9 v4.3.0 production audit follow-ups (umbrella #1176)¶
Two findings (#1179 Face Mesh + #1184 Instant Placement) accumulated post-v4.3.3 and are the primary code content of v4.3.4. Two more (#1183 EIS auto-place + #1182 snap-fling) shipped on main before the v4.3.3 tag was cut but were not formally announced in the v4.3.3 body — they are written up here for completeness.
-
AR Face Mesh — full black surface on Pixel 9 (#1179 — PR #1198) —
samples/android-demo/.../ARFaceDemo.ktno longer passescameraExposure = -1.5f. The author had intended a "-1.5 EV bias", but Filament's single-argCameraComponent.setExposure(Float)is an absolute linear exposure scaling (1.0 ≈ ISO 100 ≈ EV 0), not a signed EV-stop bias as the prior KDoc misleadingly hinted. A negative scaling clamps the framebuffer to zero, hence the fully-black scene on Pixel 9 v4.3.0 production. The front-camera AR session already force-DISABLES light estimation (seeArSession.kt) and the newARDefaultCameraNodedefaults (f/12, 1/200 s, ISO 200 ≈ EV 11.6 — after PR #1088) + 10k+3k lux main+fill lights give a correctly exposed selfie preview on every device tested. Also rewrote thecameraExposureparameter KDoc inARScene.ktso future contributors don't repeat the misinterpretation. Pinned byARCompletenessDefaultsTest.ARFaceDemo no longer passes a negative cameraExposure valueso any grep-and-paste regression gets caught. -
AR Instant Placement — anchors silently floating after
STOPPED(#1184 — PR #1198) —samples/android-demo/.../ARInstantPlacementDemo.ktnow reconciles each placed anchor'sTrackingStateevery frame. When ARCore drops a placedInstantPlacementPoint's underlyingAnchortoSTOPPED(the user typically panned the camera away from where the point was approximated), we now detach the dead anchor, hide itsModelNode(which previously froze at the last good pose, visually "floating off into space"), and surface "Lost — tap to re-place" on the per-model badge. The top status pill gains a "N lost" segment when relevant. The per-model badge column iteratesplacedModelsrather thantrackingMethodsso anchors that flip toSTOPPEDbefore their firsttrackingMethodever fires still surface as Lost. -
AR Image Stabilization (EIS) — demo auto-places helmet on first tracking frame (#1183 — PR #1191) —
ARImageStabilizationDemonow auto-creates a 1 m-in-front anchor on the first stableTRACKINGframe and drops the helmet there, with a one-shotautoPlacedguard so Clear + manual tap still hand control back to the user. The v4.3.0 demo shipped with no model visible at start — users had to wait for the plane finder (5–10 s indoors) and tap, but the "How to test" panel never said so. Pixel 9 audit frames (key-frames/t340s.jpg/t360s.jpg) showed a 30-second EIS-toggle session where the user never saw a model. With the auto-place, the demo's core value (helmet stays glued while background stabilizes) is visible within ~1 s of TRACKING. The anchor pose isframe.camera.pose.compose(Pose.makeTranslation(0f, 0f, -1.0f))so the helmet appears straight ahead at eye level regardless of camera tilt, and works in featureless areas where the plane finder stalls. -
Sketchfab carousels — snap-to-card fling + edge padding (#1182 — PR #1196) — Both Explore-tab
LazyRows (curated samples + Sketchfab feed) gainflingBehavior = rememberSnapFlingBehavior(state)so scroll releases always land on a card boundary, never mid-card, pluscontentPadding = PaddingValues(horizontal = 4.dp)for first/last-card breathing room. The Pixel 9 audit caught two Sketchfab cards (queGRD,Myślinice) rendering truncated mid-name at a viewport edge — the cards themselves were fine (maxLines = 1, overflow = TextOverflow.Ellipsis), but the LazyRow released the flick mid-card. iOS has its ownScrollView/LazyHStacksnapping config and is intentionally not touched here.
v4.3.3 — AR production hotfix: actionable Cloud Anchor error + CI key guard (2026-05-14)¶
Fixed — AR production blockers (Pixel 9 v4.3.0 audit umbrella #1176)¶
This hotfix follows the v4.3.0 production audit. The umbrella's P0 / P1 code bugs all landed by v4.3.2 (PR #1136 AR IBL baseline + #1086 HDR specular filter + #1088 AR exposure + #1075 3D IBL intensity + #1190 R8 keep rules for Fused Location Provider). v4.3.3 closes the remaining production-blocker gap that requires a Cloud-Console-side change to fully unblock end users.
-
Cloud Anchor
ERROR_NOT_AUTHORIZEDnow surfaces actionable guidance (#1177) — Whenhost()orresolve()comes back withERROR_NOT_AUTHORIZED, the demo status banner now says"The ARCore Cloud API key is rejecting this APK's SHA-1. See STREETSCAPE_SETUP.md → \"Play App Signing key\"."instead of the raw enum. The root cause on a fresh Play Store deploy is that the App Signing key SHA-1 (post-Play-resign) isn't whitelisted on the Google Cloud API key — a manual Cloud Console step that the demo can't perform itself. -
STREETSCAPE_SETUP.mdadds a "Play App Signing key" runbook — Step-by-step for maintainers to add the post-resign SHA-1 fingerprint to the ARCore API key restrictions, eliminating the production blocker without re-cutting a release. -
CI guard for ARCore key wiring (
.claude/scripts/verify-arcore-key.sh) —play-store.ymlnow fails fast ifARCORE_API_KEYsecret is missing, ifsamples/android-demo/build.gradleno longer injects thearcoreApiKeymanifest placeholder, or ifAndroidManifest.xmldrops the${arcoreApiKey}reference. Catches the silent-regression class that ships an AAB with an unwired Cloud key.
Verified fixed (closing tracker issues)¶
-
#1097
spherePlaneResponsewrong contact point on negative side — fixed inCollisionResponse.kt(contactPoint = center - planeNormal * signedDistprojects along the original unflipped normal). JVM regression testspherePlaneResponseContactPointLandsOnPlaneOnEitherSidepins the behaviour on both sides of the plane. -
#1178 AR Terrain & Rooftop Anchors fail in release builds (R8 strip) — fixed in
arsceneview/consumer-rules.provia PR #1190. Consumer-side R8 now keepscom.google.android.gms.location.**,common.api.**, andtasks.**so ARCore can reflectively link Fused Location Provider whenConfig.GeospatialMode.ENABLED. -
#1061 AR rendering quality umbrella (multiplicative drift, no default IBL, mirror reflections, EV15 vs EV11.6 exposure) — all P0 / P1 sub-issues closed: #1062 (baseline-relative light apply pattern in
ARScene.kt,AtomicReferencebaselines), #1063 (neutral IBL fallback increateAREnvironment), #1064 (environmentalHdrSpecularFilter = truedefault inLightEstimator.kt), #1067 (AR exposure aligned to v4.1.0 3D defaults).Config.LightEstimationMode.ENVIRONMENTAL_HDRis the default inARScene.ktso PBR materials read ARCore's HDR cubemap + spherical harmonics + main-light estimate from frame one. Remaining sub-issues #1065 (recording resolution) and #1066 (camera-stream double-gamma) stay open as P1 polish for v4.4.
v4.3.2 — #1152 Sketchfab streaming complete + iOS key + DemoScaffold v2 + APK slim (2026-05-14)¶
Security — fast-xml-parser bumped to 5.7.0+ via npm overrides (Dependabot alert #139 — PR #1162)¶
Resolves CVE-2026-41650 / GHSA-gh4j-gqv2-49f6 — fast-xml-parser XMLBuilder fails to escape --> (comment) and ]]> (CDATA) delimiters, allowing XML injection / XSS / SOAP-injection when user-controlled data flows into those contexts.
- Package:
fast-xml-parser(npm, dev-only transitive inreact-native/react-native-sceneview). - Resolved version before fix:
4.5.6→ after fix:5.8.0. - Severity: moderate (CVSS 6.1).
- Dependency chain:
react-native(devDep) →@react-native-community/cli-platform-ios@11.4.1→fast-xml-parser@^4.0.12.
Fix shipped as an npm overrides block in react-native/react-native-sceneview/package.json — the standard npm 8+ way to force a safe transitive version without migrating react-native from 0.72 to a newer line. npm install --package-lock-only regenerated the lockfile cleanly; npm audit reports found 0 vulnerabilities. Dev-only chain (every entry in the affected closure is "dev": true); no published runtime artefact from @sceneview-sdk/react-native ships fast-xml-parser.
Changed — Stage 3 polish + APK slim-down + Credits sheet for streamed assets (#1152 — Stage 3)¶
Stage 3 closes the Sketchfab streaming umbrella (Stage 1 foundations, Stage 2 × 8 demo migrations, Stage 3 polish, Stage 4 docs). Four polish items shipped here:
APK / IPA slim-down. samples/android-demo/src/main/assets/models/animated_dragon.glb (8.0 MB) and samples/ios-demo/SceneViewDemo/Models/animated_dragon.usdz (8.6 MB) are removed. Both files were used as canonical picks by OrbitalARDemo + ArViewTab (Android) and OrbitalARDemo + ARTab + ExploreTab (iOS). Canonical references migrate to threejs_soldier.glb (2.1 MB, animated peer) on Android and phoenix_bird.usdz (1.1 MB, animated peer) on iOS. Fallback paths in SampleAssets.kt for streamed slugs (butterfly / hummingbird / bee / koi / songbird) flip from animated_dragon.glb to threejs_soldier.glb. Net Android release-APK savings ~5 MB (88 MB → 88 MB after measurement, was 93 MB before); ~8 MB AAB on-disk. iOS IPA savings ~8.6 MB.
Credits sheet (CC-BY attribution). New samples/android-demo/.../ui/CreditsSheet.kt + samples/ios-demo/SceneViewDemo/Views/CreditsSheet.swift ModalBottomSheet / SwiftUI sheet listing every streamed Sketchfab model the demo app may load, grouped by SketchfabSlug.category, with author + CC-BY 4.0 attribution + tap-to-open-Sketchfab-page rows. Anchored to the "Credits" card on the About tab. The sheet reads SampleAssets.all directly — adding a slug in the registry automatically credits it here. CC-BY 4.0 requires visible attribution; without this sheet, redistributing the streamed models violated the license.
Per-demo offline indicator chip. New AssetSourceState enum (Streamed / Streaming / Bundled) + optional assetSource: parameter on DemoScaffold. The chip is pinned to the top-end of the scene area, advertises the streamed-or-fallback origin of the currently visible asset, and auto-hides when null. Wired into OrbitalARDemo / SceneGalleryDemo / ModelViewerDemo / ARPlacementDemo as exemplars; remaining Stage 2 demos can opt in incrementally. Helps users (and reviewers) understand at a glance whether they're seeing the streamed CC-BY model or the bundled offline fallback.
iOS parity audit. OrbitalARDemo / SceneGalleryDemo / MaterialsDemo already stream via SketchfabAssetResolver (Stage 2 parity preserved). ModelViewerDemo / AnimationDemo / MultiModelDemo / ARPlacementDemo / ARInstantPlacementDemo / PhysicsDemo are Android-only in v4.3.x; per feedback_ios_mirror_android.md iOS V1 ships as a strict subset. Follow-up issue filed to track porting (see issue body — Stage 3 PR creation).
Cleanup. SketchfabSlug.sketchfabUrl computed property added on both platforms (link target for the Credits sheet). assets/CREDITS.md keeps the dragon entry for posterity — the model is still on Sketchfab and the CDN-hosted GLB at cdn.jsdelivr.net/.../assets/models/glb/animated_dragon.glb did not exist anyway (web-demo dragon entry was a dead link before this PR; now removed).
Added — In-app auto-update across every sample app¶
Every published sample app now checks for a newer build on resume and surfaces a banner that lets the user trigger the install in a single tap. The pattern stays in samples/ rather than the SceneView SDK itself — auto-update isn't a 3D/AR concern, and bundling Play Core / iTunes plumbing into sceneview-core would force every consumer to ship it.
Android (samples/android-demo, samples/android-tv-demo). io.github.sceneview.sample.common.update.InAppUpdateManager is now factored into :samples:common and wraps Play Core's AppUpdateManager.startUpdateFlow(FLEXIBLE). The matching UpdateBanner composable renders during DOWNLOADING / READY_TO_INSTALL only, with a "Restart" CTA that calls completeUpdate(). samples/android-demo's previous in-tree copy is deleted in favour of the common one; android-tv-demo gains the INTERNET permission + a TV-friendly banner overlay focused on Alignment.TopCenter. A secondary constructor allows tests to inject FakeAppUpdateManager directly. Seven Robolectric tests cover IDLE → DOWNLOADING → READY_TO_INSTALL → IDLE, checkForStalledUpdate (download finished while backgrounded), destroy() idempotency, FLEXIBLE-type sanity, and zero-totalBytes safety.
iOS (samples/ios-demo). New AppStoreUpdater ObservableObject hits https://itunes.apple.com/lookup?id=6761329763 on every ScenePhase.active transition, compares the result with Bundle.main.infoDictionary["CFBundleShortVersionString"], and renders a Liquid Glass .regularMaterial SwiftUI banner with Update (deep-links to itms-apps://itunes.apple.com/app/id...) + Later (7-day snooze) CTAs. Throttle: 12 h between network calls via UserDefaults; snooze key cleared after the window expires. Apple does not expose a programmatic install API on iOS, so the banner is the best we can do — documented in the manager's KDoc. XCTest fixture (SceneViewDemoTests/AppStoreUpdaterTests.swift) ships with a URLProtocol stub harness; the project-level test target wiring lands in a follow-up PR.
Web (samples/web-demo). document.addEventListener('visibilitychange') polls https://sceneview.github.io/version.json (cached for 12 h via localStorage) and slides a Liquid Glass snackbar from the bottom with a Reload CTA when the JSON reports a version newer than the build-time BUILD_VERSION constant. Snooze is keyed on the latest seen version so a future bump re-surfaces the prompt. New website-static/version.json is auto-deployed by the existing deploy-website.yml workflow at every website-static/** push — sync-versions.sh keeps the .version field in lockstep with gradle.properties VERSION_NAME.
Flutter (samples/flutter-demo). WidgetsBindingObserver triggers UpdateChecker.checkForUpdate() on AppLifecycleState.resumed. Android delegates to in_app_update (the community wrap of Play Core); iOS uses http + package_info_plus to read the iTunes lookup response and url_launcher to open itms-apps://. Material 3 banner surfaces the same Update / Later CTAs as the other platforms.
React Native (samples/react-native-demo). <UpdateChecker /> mounts at the root; AppState events drive the check, Android via sp-react-native-in-app-updates, iOS via fetch + Linking.openURL. New cross-platform 12 h throttle + 7-day snooze in component state. RN demo version literal bumped 3.6.2 → 4.3.1 to align with gradle.properties.
Infrastructure. .claude/scripts/sync-versions.sh gains 5 new checks (website-static/version.json .version field, web-demo Main.kt SDK_VERSION, web-demo index.html BUILD_VERSION literal, RN-demo package.json "version", RN-demo App.tsx VERSION literal) with matching --fix paths. llms.txt (mirrored to docs/docs/llms.txt) documents the pattern AI-first so a developer asking an AI to add auto-update to their SceneView app gets working code on the first try.
Added — Stage 4 docs + AI-first surfaces for Sketchfab streaming + DemoScaffold v2 (#1152 — Stage 4)¶
Stage 4 of the #1152 umbrella. The Stage 2 patterns shipped over the last 7 PRs (Sketchfab streaming + DemoScaffold v2 modal sheet + chip picker) now have first-class documentation on every AI-first surface SceneView exposes.
New recipe pages (mkdocs).
docs/docs/recipes/sketchfab-streaming.md— full how-to + license guidance + add-a-slug checklist + API-key wiring story.docs/docs/recipes/demo-settings-sheet.md—DemoScaffoldv2 API + picker pattern + gesture map + discoverability lesson from issue #951.docs/mkdocs.yml— nav restructured. "Recipes" was a single leaf; now it's a section with Overview + the two new recipe pages.
llms.txt updates (root + docs/docs/llms.txt mirror).
Two new sections inserted before "Android Advanced APIs":
## Sketchfab streaming for samples (#1152)— copy-paste resolver pattern (8 lines of Kotlin) + hard rules (CC-BY-only, no WebView, never network-required, attribute the author) + LRU cache contract + bounds sanity check.## DemoScaffold v2 — full-screen scene + ModalBottomSheet controls (#1154)— DemoScaffold API signature + picker pattern + gesture map.
docs/docs/llms.txt synced byte-for-byte to root via cp.
New MCP resources (sceneview-mcp npm package).
Two new examples:// URIs surface compact (< 4 KB each) inline examples that an AI agent can fetch in one round-trip when it needs to scaffold a demo:
examples://demo-with-settings— DemoScaffold v2 pattern.examples://sketchfab-streaming— SketchfabAssetResolver pattern.
Both are registered in mcp/src/index.ts's ListResourcesRequestSchema + ReadResourceRequestSchema handlers. Body strings live in a new mcp/src/examples.ts module so the build pipeline can pin their byte budget via mcp/src/examples.test.ts (16 new vitest cases — start with H1, mention key APIs, < 4 KB, point at full recipe).
Files touched:
docs/docs/recipes/sketchfab-streaming.md(new) — full how-to.docs/docs/recipes/demo-settings-sheet.md(new) — full how-to.docs/mkdocs.yml— Recipes section restructured.llms.txt+docs/docs/llms.txt— 2 new sections + version-resync to 4.3.1.mcp/src/examples.ts(new) — inline resource bodies.mcp/src/examples.test.ts(new) — 16 vitest cases pin the resource shape.mcp/src/index.ts— wires the 2 new resources into theListResourcesRequestSchema+ReadResourceRequestSchemahandlers.mcp/src/generated/llms-txt.ts— regenerated from rootllms.txt(the build pipeline embeds it viamcp/scripts/generate-llms-txt.js).mcp/src/__fixtures__/analyze-project/android-ok/build.gradle.kts— fixture bumped from 4.1.2 to 4.3.1 bymcp/scripts/generate-version.jsrunning duringnpm run prepare.
Acceptance:
cd mcp && npm testGREEN (2562 tests, 102 files — 16 new fromexamples.test.ts).bash .claude/scripts/sync-versions.shGREEN (0 errors, 1 pre-existing warning).cp llms.txt docs/docs/llms.txt— diff is now empty.
Changed — Stage 2 demo migrations: PhysicsDemo drops streamed crash-test bodies (#1152 — Stage 2)¶
samples/android-demo/.../demos/PhysicsDemo.kt keeps the existing PhysicsNode-driven simulation but replaces the coloured spheres carousel with the four streamed entries from SampleAssets.byCategory["physics"] — Ceramic Vase, Wooden Stool, Wooden Barrel, Clay Amphora (all CC-BY from Sketchfab). A first "Bundled spheres" chip preserves the v4.3.1 visual default for QA / offline / store-listing screenshot determinism.
Behavioural contract. The simulation is unchanged — every dropped body is treated as a bounding-sphere of collisionRadius = 0.08 m so the bounce reads naturally regardless of mesh shape. The visual mesh is a ModelNode parented to the simulated SphereNode; the parent sphere is still drawn (the colour ramp gives a soft pad underneath the streamed mesh) so the simulation feels like "spheres with mesh skins" rather than abstract solids. This honours feedback_demo_quality — the demo's value is the SDK simulation hook-up, not a custom physics engine that handles convex-hull colliders.
Switching the picker resets the scene (bodyCount = 5; generation++) so the new shape is what falls — useful because mixed scenes confuse what the user is supposed to be observing.
Offline / no-key behaviour preserved — the resolver's per-slug fallback path returns the registered bundled GLB even when SketchfabConfig.apiKey == null, so the carousel always renders something visible. The streamed slot will visually match the bundled fallback in that case.
Files touched:
samples/android-demo/.../demos/PhysicsDemo.kt— full rewrite of the composable. Adds the chip row, the slug resolver, and the streamed-mesh-as-child pattern.samples/android-demo/src/main/res/values/strings.xml+values-fr/strings.xml— 3 new keys:demo_physics_picker_label,demo_physics_picker_spheres,demo_physics_picker_subtitle.
iOS counterpart not in this PR. The iOS demo app does not currently have a PhysicsDemo.swift — RealityKit's built-in PhysicsBodyComponent makes the SceneView wrapper less interesting on iOS, and the iOS V1 doesn't expose a SceneView PhysicsNode analogue. The 4 physics slugs (2 new in the AR-placement PR, 2 from Stage 1) are registered in samples/ios-demo/.../Services/SampleAssets.swift ready for a future port.
SampleAssets slugs added: 0 — the 2 new physics entries (Wooden Barrel, Clay Amphora) shipped in the previous Stage 2 PR (PR #1187 AR placement); this PR consumes them for the first time.
30 s screen recording deferred — agent worktree has no Pixel device access; tracked in the #1152 acceptance checklist.
Changed — Stage 2 demo migrations: ARPlacementDemo + ARInstantPlacementDemo gain a "Pick what to place" sheet (#1152 — Stage 2)¶
Both AR placement demos now expose the SampleAssets.byCategory["ar_placement"] chip row in their DemoScaffold v2 controls sheet (delivered in PR #1169). Selecting a streamed slug (coffee mug / houseplant / wooden crate / side table / floor lamp / picture frame — six entries CC-BY from Sketchfab) arms it as the next tap's payload; subsequent taps on a detected plane spawn a fresh AnchorNode + ModelNode using the streamed glTF resolved through SketchfabAssetResolver.
A first "Bundled cycle" chip preserves the v4.3.1 behaviour — each tap rotates through the existing 5-model bundled GLB cycle (helmet / fox / lantern / toy car / shiba). This keeps the demo deterministic for QA / offline / store-listing screenshots and gives the user a clear "no surprises" mode side-by-side with the streamed picker.
Behavioural contract:
- Selected slug, download landed. Tap places the streamed slug. Multiple taps place multiple instances of the same slug.
- Selected slug, download still in flight. Tap silently falls back to the bundled cycle so the tap is never lost. The picker subtitle shows "Streaming X…" so the user knows the streamed pick will activate on the next tap.
- "Bundled cycle" selected. v4.3.1 behaviour preserved.
Offline / no-key behaviour preserved — the resolver's per-slug fallback path still returns the registered bundled GLB even when SketchfabConfig.apiKey == null, so a tap on a streamed chip always renders something. The streamed slot will visually match the bundled fallback in that case, which is the same trade-off Stage 1 documented.
Files touched:
samples/android-demo/.../demos/ARPlacementDemo.kt— adds the chip row, the slug resolver, the per-tap "selected vs cycle" decision.PlacedModel.assetPathrenamed toassetLocationso bothassets/-relative paths andfile://URIs flow through the samerememberModelInstancecall.samples/android-demo/.../demos/ARInstantPlacementDemo.kt— same chip row, hoisted to the outerARInstantPlacementDemocomposable so it survives thekey(instantEnabled)rebuild that re-creates the inner ARCore session.samples/android-demo/src/main/res/values/strings.xml+values-fr/strings.xml— 5 new keys:demo_ar_placement_picker_label,demo_ar_placement_picker_bundled,demo_ar_placement_picker_streaming,demo_ar_placement_picker_streamed,demo_ar_placement_picker_subtitle.samples/android-demo/.../sketchfab/SampleAssets.kt+samples/ios-demo/.../Services/SampleAssets.swift— growar_placementfrom 3 to 6 entries (Side Table, Floor Lamp, Picture Frame added) so the picker has IKEA-showroom variety. iOS registry mirrored 1:1 for future Swift port.
iOS counterpart not in this PR. The iOS demo app (samples/ios-demo) does not currently have an ARPlacementDemo.swift — the iOS V1 didn't port the tap-to-place AR flow. The 3 new ar_placement slugs are registered in iOS SampleAssets.swift ready for a future port; the iOS demo file itself is deferred. ARKit's RealityKit.AnchorEntity(plane:) factory shipped in v4.2.0 (#1025) — the iOS port mostly needs a SwiftUI chip row + the existing resolver glue.
SampleAssets slugs added: 6 — 3 new ar_placement (Side Table, Floor Lamp, Picture Frame) + 2 new physics (Wooden Barrel, Clay Amphora) + 1 (Editor's note: see PhysicsDemo PR) that pairs with the next Stage 2 PR. All CC-BY 4.0.
30 s screen recording deferred — agent worktree has no Pixel device access; tracked in the #1152 acceptance checklist.
Changed — Stage 2 demo migrations: MultiModelDemo composes the streamed "Park" scene (#1152 — Stage 2)¶
samples/android-demo/.../demos/MultiModelDemo.kt swaps its tabletop arrangement of bundled assets (shiba + lantern + helmet + dragon) for the streamed "Park" scene composition — oak tree (backdrop) + park bench (foreground prop) + idle dog + perched songbird, all four resolved through SketchfabAssetResolver from the new park category of SampleAssets.
The composed scene now actually showcases what "multi model" means in practice — a real outdoor vignette where each asset comes from a different author / source / tool, all unified by studio_warm_2k.hdr and the shared scene-yaw rotation. The dog + bird carry skeletal animations so the scene reads as alive instead of as a still life. Two models are static (tree, bench), two are animated (dog, bird) — the same 2/2 alive-vs-still ratio the original tabletop had.
Visibility chips kept the same shape (one chip per node) but renamed Tree / Bench / Dog / Bird. The "Spin scene" toggle and the per-model rotation cancellation are unchanged.
Offline behaviour preserved — each streamed slot falls back to its registered bundled GLB / USDZ (Android: khronos_lantern.glb for tree + bench, shiba.glb for the dog, animated_dragon.glb for the bird; iOS: tree_scene.usdz / fantasy_book.usdz / animated_butterfly.usdz / phoenix_bird.usdz). The scene composition stays four-distinct-nodes even when offline.
SampleAssets slugs added: 4 new entries in a new park category — Oak Tree (1ca42d9d…), Park Bench (92a4c3ad…), Idle Dog (62fadcf9…), Songbird (8e7a3a8a…). All CC-BY 4.0. The SampleAssetsTest.every Stage 2 category is represented test now expects park in the category set.
prefetchAll("park") is called from a LaunchedEffect(Unit) on first composition so the four streams kick off in parallel before the user has finished reading the controls panel. Each per-node resolve later picks up the cached file via the resolver's dedup logic.
iOS counterpart not in this PR. The iOS demo app (samples/ios-demo) does not currently have a MultiModelDemo.swift — the iOS V1 didn't port the multi-model scene. The 4 park slugs are registered in iOS SampleAssets.swift ready for a future port, but the Swift demo file itself is deferred.
30 s screen recording deferred — agent worktree has no Pixel / iPhone device access; tracked in the #1152 acceptance checklist.
Changed — Stage 2 demo migrations: AnimationDemo carousel of 5 animated models from the animation category (#1152 — Stage 2)¶
samples/android-demo/.../demos/AnimationDemo.kt is no longer locked to a single hard-coded threejs_soldier.glb. A new "Subject" chip row above the existing Camera row lets the user cycle through 5 animated models — the bundled soldier (slot 0, preserves the v4.3.1 default for visual stability) plus the four streamed entries of the animation category in SampleAssets: Walking Robot, Dancing Knight, Idle Cat, Sleeping Fox.
Switching subjects rebinds the play/pause/speed/loop controls + the animation-name chip row to the new model — playAnimation/stopAnimation use the active model's animation count, so out-of-range indices are clamped automatically when going from a 4-animation soldier to a 1-animation streamed creature. The model lift is now derived from scaleToUnits (was hard-coded position.y = 0.5), so the feet stay grounded at y=0 for every model regardless of scale.
Offline behaviour preserved — when SketchfabConfig.apiKey == null, each streamed slot falls back to the registered bundled GLB (threejs_soldier.glb / shiba.glb / khronos_fox.glb), so the carousel always has 5 working entries (some may look like duplicates in offline mode, which is the same trade-off Stage 1 documented).
iOS counterpart skipped this PR. iOS AutoRotateDemo.swift is the iOS V1 stand-in for the Android AnimationDemo and renders a non-animated metallic torus — there's no skeletal-rig playback on iOS yet (tracked in the v4.3.0 parity backlog, see #1004 iOS parity umbrella). Migrating it requires the iOS skinning port first.
SampleAssets slugs added: 0. The four animation slugs shipped in Stage 1 already.
30 s screen recording deferred — agent worktree has no Pixel device access; tracked in the #1152 acceptance checklist.
Added — Stage 2 demo migrations: MaterialsDemo streams the curated materials category (#1152 — Stage 2)¶
Third Stage 2 migration. The previous MaterialsDemo (5-sphere metallic/roughness spectrum) didn't actually exercise any of the modern glTF material extensions — it was a hand-built PBR sweep useful for diagnosing the renderer, not for answering "what does KHR_materials_sheen look like in SceneView?". Stage 2 replaces it on both platforms with the curated extension-bearing models from SampleAssets's materials category (Iridescent Beetle / Glass Decanter / Velvet Cushion — sheen, transmission, iridescence).
Why streamed. Each model carries a glTF extension that depends on the author's source PBR tooling — bundling a hand-authored stand-in would either ship a giant binary (transmission demands a full IBL backdrop) or fake the look (and mislead the AI-first contract). Streaming the real Khronos / community assets keeps the demo honest.
Files touched:
samples/android-demo/.../demos/MaterialsDemo.kt(new) — chip row + studio HDR + auto-orbit + per-chip extension tag (the registry'stags[0]is theKHR_materials_*extension name).samples/android-demo/.../DemoRegistry.kt— newmaterialsentry in theAdvancedcategory with theIcons.Filled.Paletteicon.samples/android-demo/.../MainActivity.kt— routesmaterialstoMaterialsDemo.samples/android-demo/src/main/res/values/strings.xml+values-fr/strings.xml— 4 new keys:demo_materials_title,demo_materials_subtitle,demo_materials_loading,demo_materials_credit.samples/ios-demo/SceneViewDemo/Views/Demos/MaterialsDemo.swift— rewrote the 5-sphere PBR sweep as the streamed mirror. Samematerialscategory, same chip row + extension tag + author byline,SketchfabAssetResolver.shared.resolve(slug)+ModelNode.load(contentsOf:). The existingSamplesTabentry already wires upMaterialsDemo()— no dispatch change needed.
SampleAssets slugs added: 0. The three materials slugs (Iridescent Beetle, Glass Decanter, Velvet Cushion) shipped in Stage 1 and are now consumed by this PR for the first time.
i18n hygiene. All 4 new keys ship in EN + FR. The chip labels are catalogue-authored ids (English-only, per OrbitalARDemo convention). The extension tag (KHR_materials_iridescence etc.) is a glTF extension name and intentionally not localised — it's a spec identifier developers will Google.
Screen recording. Deferred to the combined Stage 2 visual-smoke pass.
Acceptance: Android ./gradlew :samples:android-demo:compileDebugKotlin GREEN. :samples:android-demo:testDebugUnitTest --tests "io.github.sceneview.demo.sketchfab.*" GREEN (27/27 unchanged).
Added — Stage 2 demo migrations: ModelViewerDemo gains a "Surprise me" Sketchfab pick (#1152 — Stage 2)¶
Second Stage 2 migration. ModelViewerDemo keeps the bundled khronos_damaged_helmet.glb as its hero default (so screenshots / Play Store store assets stay byte-identical) and adds an ExtendedFloatingActionButton that streams a fresh downloadable Sketchfab model on demand:
- Default state. Bundled helmet, same as before. The hero shot the store-page renders promise.
- Tap "Surprise me". Calls
SketchfabService.search(query, downloadable = true, limit = 24)with a small rotating PBR-friendly query list (pbr/modern/scan), filters todownloadable && faceCount in 1..200_000(so a 5 M-poly scan doesn't stall the demo), picks a random hit, and downloads it through the sharedSketchfabServicecache. The streamed pick replaces the helmet for the rest of the session until the next tap. - No-key build. The FAB is hidden when
SketchfabConfig.apiKey == null(App Store / no-secret CI builds) — silently falling back to the same helmet would mislead users about the demo's capability. - Failure modes are silent. A 4xx / 5xx / empty-results path keeps the helmet on screen rather than going black. The
surpriseInFlightflag flips back tofalseso the user can retry.
Files touched:
samples/android-demo/.../demos/ModelViewerDemo.kt— full rewrite of the composable. Adds the FAB, the surprise coroutine, the failure-keeps-helmet contract. Streamed instance scaled to 0.4 m (vs the helmet's historical 0.3 m) so a 5 cm bee and a 5 m crate both read in the orbit sweet spot.samples/android-demo/src/main/res/values/strings.xml+values-fr/strings.xml— 3 new keys:demo_model_viewer_loading,demo_model_viewer_surprise,demo_model_viewer_surprise_loading.
iOS counterpart. No iOS file change — there is no dedicated ModelViewerDemo.swift. The iOS deep-link router already maps "model-viewer" to SceneGalleryDemo (DemoDeepLinkRegistry.swift:77), which already streams Sketchfab content (now with the Stage 2 gallery migration). The iOS Explore tab is the canonical "browse + surprise" experience on iOS.
SampleAssets slugs added: 0. The Surprise path doesn't go through the curated registry — it's a free-form Sketchfab search restricted to downloadable && PBR-friendly. The license filter on the search side is not yet a 100% guarantee of CC-BY (Sketchfab returns mixed CC variants); Stage 3 will add a license-filter pass before the model lands on screen + a Credits sheet exposing the per-pick attribution.
i18n hygiene. All three new FAB strings ship in EN + FR. No raw English leaks on the FR locale.
Screen recording. Deferred to the combined Stage 2 visual-smoke pass.
Acceptance: Android ./gradlew :samples:android-demo:compileDebugKotlin GREEN. :samples:android-demo:testDebugUnitTest --tests "io.github.sceneview.demo.sketchfab.*" GREEN (27/27 unchanged).
Added — Stage 2 demo migrations: SceneGalleryDemo streams the curated gallery category (#1152 — Stage 2)¶
First Stage 2 migration on top of the Stage 1 resolver foundations. SceneGalleryDemo is now a category-chip-driven streamed gallery on both Android and iOS — chips map 1:1 to the four gallery slugs in SampleAssets (Vintage Cassette, Polly the Parrot, Reading Lamp, Wooden Chair), the resolver hands back the streamed GLB/USDZ or the bundled fallback when no key is configured, and SceneView orbits the model. No external Sketchfab WebView — the demo only ever feeds the local file URL to rememberModelInstance (Android) / ModelNode.load(contentsOf:) (iOS).
Files touched:
samples/android-demo/.../demos/SceneGalleryDemo.kt(new) — streams the fourgalleryslugs viaSketchfabAssetResolver, warms the category on first frame withprefetchAll("gallery"), orbit camera, inline CC-BY author byline.samples/android-demo/.../DemoRegistry.kt— newscene-galleryentry in the3D Basicscategory with theIcons.Filled.Collectionsicon.samples/android-demo/.../MainActivity.kt— routesscene-gallerytoSceneGalleryDemo.samples/android-demo/src/main/res/values/strings.xml+values-fr/strings.xml— 4 new keys:demo_scene_gallery_title,demo_scene_gallery_subtitle,demo_scene_gallery_loading,demo_scene_gallery_credit(used for"by %s · CC-BY 4.0"). The chip labels themselves come from the catalogue'sSketchfabSlug.displayName(curator-authored English ids, not localizable copy).samples/ios-demo/SceneViewDemo/Views/Demos/SceneGalleryDemo.swift— rewrote the placeholder shape-pedestal scene as the cross-platform mirror: samegallerycategory, same chip row + author byline,SketchfabAssetResolver.shared.resolve(slug)+ModelNode.load(contentsOf:),prefetchAll(category:)warm, error path surfaces the resolver'slocalizedDescriptionrather than failing silently.
SampleAssets slugs added: 0. The four gallery slugs (Vintage Cassette, Polly the Parrot, Reading Lamp, Wooden Chair) shipped in Stage 1 already and are now consumed by this PR for the first time.
i18n hygiene. The chip labels render SketchfabSlug.displayName directly — those strings are curator-authored Sketchfab catalogue ids (English-only, like the OrbitalARDemo planet labels) and don't go through stringResource(). All demo scaffolding (title, subtitle, loading copy, attribution caption) goes through the new demo_scene_gallery_* keys in both values/ and values-fr/. No raw English string leaks into a non-English locale.
Screen recording. Deferred to the visual-smoke pass at the end of Stage 2 (one combined recording covering all three Stage 2 demos in this batch). Compile + unit tests gated this PR.
Acceptance: Android ./gradlew :samples:android-demo:compileDebugKotlin GREEN. :samples:android-demo:testDebugUnitTest --tests "io.github.sceneview.demo.sketchfab.*" GREEN (27 sketchfab tests passing unchanged from Stage 1). iOS xcodebuild skipped in this batch (CHANGELOG entry kept honest — Stage 1 ran the Xcode build; the SceneGalleryDemo iOS rewrite is a small file replacement with no new Swift symbols).
Changed — Stage 2 demo migrations: OrbitalARDemo streams 4 animated creatures from the solar category (#1152 — Stage 2)¶
samples/android-demo/.../demos/OrbitalARDemo.kt + samples/ios-demo/SceneViewDemo/Views/Demos/OrbitalARDemo.swift now stream four of their eight orbiting planets via SketchfabAssetResolver from the solar category of SampleAssets — butterfly, hummingbird, bee, koi fish. The remaining four planets (khronos_damaged_helmet, khronos_lantern, khronos_toy_car, animated_dragon on Android; red_car, game_boy_classic, animated_dragon, nintendo_switch on iOS) stay bundled.
Before: the 7-planet formation had to duplicate animated_dragon + threejs_soldier to fill the ring because only seven distinct GLBs ship in the APK — visible as "clones" in the #978 audit screenshot. After: eight distinct themed planets, every "alive" slot has a real baked animation, and Sketchfab is invisible to the user (no WebView, no "loading Sketchfab" UI — just rememberModelInstance(modelLoader, "file://...") once the resolver returns).
Offline behaviour preserved — when SketchfabConfig.apiKey == null (App Store builds, cold-cache first launch, network down), each streamed slot falls back to its registered bundled GLB / USDZ, so the orbit always renders eight models. No "Asset unavailable" placeholder ever surfaces from this demo.
SampleAssets slugs added: 0. The four solar slugs shipped in Stage 1 already and are consumed by this PR for the first time.
30 s screen recording deferred — agent worktree has no Pixel / iPhone device access; tracked in the #1152 acceptance checklist.
Added — Samples Sketchfab streaming foundations (#1152 — Stage 1)¶
Stage 1 of the #1152 umbrella — SketchfabAssetResolver foundations that the Stage 2 demo migrations (OrbitalARDemo, SceneGalleryDemo, AnimationDemo, MultiModelDemo, ARPlacementDemo, PhysicsDemo, MaterialsDemo) will build on. No demo is migrated in this PR — the bundled GLBs/USDZs stay as they are. The resolver, registry, and tests are the foundation; demo migrations land 1 PR per demo.
New files (Android — samples/android-demo/.../sketchfab/):
SketchfabSlug.kt— typed slug + license + scale + animation + category + author + tags. Constructor rejects any non-CC-BY 4.0 license URL, a blank author, an empty fallback path, or a non-positive scale.SampleAssets.kt— 20-entry curated CC-BY-only registry grouped into 6 Stage 2 categories:solar(4),gallery(4),animation(4),ar_placement(3),physics(2),materials(3).byUid/byCategorylookups +requireValid()for CI invariants (no duplicate uids, every uid is 32-char lowercase hex).SketchfabAssetResolver.kt—resolve(slug)/prefetchAll(category)/ LRU eviction (250 MB cap, tighter than the Explore-tab 500 MB cap) / bounds sanity check (magic-byte + size floor) / fallback-to-bundle when no key OR network fails. WrapsSketchfabServicewith exponential backoff (429/5xx only, max 3 retries) and falls back immediately on policy-decision 4xx.
New files (iOS — samples/ios-demo/SceneViewDemo/Services/):
SketchfabSlug.swift,SampleAssets.swift,SketchfabAssetResolver.swift— same 20-uid registry, same resolver semantics, RealityKit-compatible (accepts both GLBglTFmagic and USDZ ZIPPK\x03\x04magic in the bounds check).actorfor theURLSessionserialisation invariant that matchesSketchfabService.SketchfabAssetResolver+Tests.swift— XCTest mirror of the Android suite (no livexcodebuild testtarget wires it up yet; the file lives next to the existingSketchfabService+Tests.swiftscaffold for documentation parity).
Tests (Android — 24 new unit tests, all passing):
SampleAssetsTest.kt— 13 tests: registry non-empty, every entry CC-BY 4.0, every entry has a non-blank author, every entry has a fallback, scale in[0.05 m, 5 m], no duplicate uids,requireValidsucceeds,byUid/byCategoryagree withall, all 6 Stage 2 categories represented, constructor rejects non-CC-BY / blank author / non-positive scale.SketchfabAssetResolverTest.kt— 11 tests:resolvefalls back without an API key,Unknownfor slugs outside the registry,boundsAreSanerejects 0-byte/junk/missing files and accepts a real GLB header,pruneCacheis a no-op sub-budget,FallbackUnavailablewhen the bundled asset is missing,prefetchAllreturns 0 for unknown categories, singleton wiring.
Hard rules honoured (Stage 1 = pure plumbing):
- NEVER ship a build that needs the network to render something useful. Every
SketchfabSlugcarries afallbackBundledPaththat already lives in the demo APK / IPA. The resolver returns it whenever the API key is absent (App Store builds), the network fails, or the streamed asset fails the magic-byte sanity check. - NEVER open a Sketchfab WebView / external link. The resolver returns a local
File/URLonly; consumers feed it intorememberModelInstance(modelLoader, file)/ RealityKitEntity.load(...). - CC-BY only. Every entry's
licenseUrlishttps://creativecommons.org/licenses/by/4.0/. Other Creative Commons variants (NC, ND, SA) and the bespoke "Sketchfab Standard" license are rejected bySketchfabSlug.init. - Cache survives across demos. Resolver uses the same
cacheDir/sketchfab/directory asSketchfabService, so a model warmed by the Explore tab is reused by Stage 2 demos.
Stage 1 status note. The 20 placeholder uids in SampleAssets were curated at design time but are not yet validated against GET /v3/models/<uid>. Stage 2 PRs will replace each uid with one verified live (Sketchfab maintainer account check) AND add a weekly CI cron that pings each slug + opens a GitHub issue on 404 / license drift. The licenseURL + fallbackBundledPath columns are authoritative even today — they decide what the resolver hands a demo offline.
Acceptance: Android ./gradlew :samples:android-demo:compileDebugKotlin + :samples:android-demo:testDebugUnitTest --tests "io.github.sceneview.demo.sketchfab.*" GREEN (27 sketchfab tests passing — 24 new + 3 pre-existing). iOS xcodebuild -scheme SceneViewDemo … build GREEN (3 new Swift files compile, project added them to the SceneViewDemo target).
Fixed — iOS: SKETCHFAB_API_KEY never reached TestFlight + App Store binaries (#1157)¶
Every iOS app-store ship since v3.6 silently degraded the Explore tab to bundled fallback models because the Sketchfab API key never made it into the .ipa. Two compounding root causes:
SketchfabConfig.swiftread the key viaProcessInfo.processInfo.environment["SKETCHFAB_API_KEY"]— that path only works under Xcode's "Run" scheme. CI env vars set on the runner don't survivexcodebuild archiveinto the shipped binary, soSketchfabConfig.apiKey == nilfor every TestFlight + App Store build →SketchfabError.missingApiKey→ExploreTabrunCatchingswallow → empty / fallback results with no error banner..github/workflows/app-store.ymlandios.ymlnever referencedSKETCHFAB_API_KEY— confirmed bygrep. The Android pipelines (play-store.yml:170,build-apks.yml:47) inject the secret correctly and Android'sBuildConfig.SKETCHFAB_API_KEYbakes it in at compile time, which is why Play Store builds were unaffected.
Fix (single PR, 4 files):
samples/ios-demo/SceneViewDemo/Services/SketchfabConfig.swift—apiKeynow resolves fromBundle.main.object(forInfoDictionaryKey: "SketchfabAPIKey")first, with a guard that rejects the unsubstituted$(SKETCHFAB_API_KEY)xcconfig token literal. LegacyProcessInfolookup stays as a fallback so the Xcode "Run" scheme env-var workflow keeps working for contributors.samples/ios-demo/SceneViewDemo/Info.plist— addedSketchfabAPIKey = $(SKETCHFAB_API_KEY)placeholder.xcodebuildsubstitutes it from the user-defined build setting at archive time..github/workflows/app-store.yml— both iOS and macOSxcodebuild archivesteps now passSKETCHFAB_API_KEY="$SKETCHFAB_API_KEY"(sourced from theSKETCHFAB_API_KEYrepo secret)..github/workflows/ios.yml— same injection on the CI demo-build step so theInfo.plistsubstitution path is exercised on every PR, not just on release tags.
Verified locally on Xcode 26.3 / iPhone 16e simulator: xcodebuild build … SKETCHFAB_API_KEY=dummy_key_for_test produces a SceneViewDemo.app/Info.plist with SketchfabAPIKey = dummy_key_for_test (vs. the literal $(SKETCHFAB_API_KEY) placeholder without the build setting). Acceptance: next TestFlight build of v4.3.2+ surfaces non-empty SketchfabConfig.apiKey and ExploreTab shows live Sketchfab categories + search.
Long-term proxy via mcp-gateway so end-user binaries don't ship the master key is tracked by the V1.1 TODO in SketchfabConfig.swift — this fix is the immediate "Explore tab works again" patch.
Tests — Regression pins for v4.3.0 rendering-burst fixes that shipped without coverage (#1120 extension)¶
Follow-up to the CORR-C regression-pin batch (PR #1137). Three of the v4.3.0 fixes shipped without test coverage because the failure modes required Filament JNI (CORR-C's pure-JVM batch couldn't reach them). This extension adds the missing instrumented tests so a future refactor catches the regression at ./gradlew :connectedDebugAndroidTest time:
sceneview/src/androidTest/.../RenderQualityComposeTest.kt— Filament-grounded companion to the JVMRenderQualityLaunchedEffectTest. Pins the #1078 keyed-LaunchedEffect(view, renderQuality)contract using a realView: apply the preset, mutateview.bloomOptions.strength = 0.4f, simulate 5 unchanged recompositions, assert the user tweak survived. Pre-#1078 (unkeyedSideEffect), the 0.4f would have been clobbered back to the preset value on every recomposition. 3 test methods. The two pure-JVMRenderQualityLaunchedEffectTest+ instrumentedRenderQualityComposeTestcover the contract from both angles — JVM catches the LaunchedEffect re-keying semantics, instrumented catches the Filament-side preset-application invariants.sceneview/src/androidTest/.../node/CameraNodeLifecycleTest.kt— Pins theDisposableEffect(cameraNode)rewire shipped in PR #1147 (Scene.kt:293, closes #1143). Three tests: 5 sequentialSceneNodeManagerlifecycles sharing one FilamentSceneleak zero cameras, parent → child HUD-node propagation cascades on dispose, and thecameraNodeswap path replaces cleanly without leaking the previous instance. Same-family check as the #1122 light-node leak fix (PR #1131).samples/android-demo/src/androidTest/.../MaterialInstanceLeakTest.kt— Pins thedestroyMaterialsOnDispose: Boolean = falseflag added toRenderableNode+GeometryNodeconstructors in PR #1132 (closes #1123). Four tests: the flag actually destroys the constructor-passedMaterialInstance(itsnativeObjecthandle drops to0), defaultfalsepreserves the instance for external owners (rememberMaterialInstance,DisposableEffect), multi-primitive lists withnullentries are handled without NPE, and the destroy path is idempotent across double-destroy via therunCatching-wrappedsafeDestroyMaterialInstance.arsceneview/src/androidTest/.../light/LightEstimatorConcurrentDestroyTest.kt— already shipped as part of PR #1148 (#1094 acceptance #3); listed here for traceability.
The pure-JVM RenderQualityLaunchedEffectTest and LightEstimatorConcurrentDestroyTest from CORR-C continue to run on every :sceneview:test invocation; the instrumented tests above run on ./gradlew :sceneview:connectedDebugAndroidTest / :samples:android-demo:connectedDebugAndroidTest. Net +3 instrumented test files / +10 test methods.
1123 acceptance criterion "at least 1 demo migrated to use destroyMaterialsOnDispose = true" stays open — surfacing the flag through the Compose SceneScope.CubeNode / SphereNode / etc. factories is a separate API extension. MaterialInstanceLeakTest pins the library-level contract those factories will eventually wire up.¶
v4.3.1 — CI hardening + iOS AR LightSlot parity + i18n migration (2026-05-14)¶
CI hardening + docs accuracy + Android CLI migration + one v4.1.0-stale demo light tune,
plus the second half of #1063 ported to iOS (LightSlot + .fillLight(_:) on ARSceneView)
and a full android-demo UI migration to stringResource(R.string.…) so French locale
actually flips at runtime. No new Android public API; one new iOS surface.
Changed — Demo UX: DemoScaffold v2 ships the controls in a ModalBottomSheet (#1154)¶
The 35 Android demos no longer split their viewport 60 / 40 between scene and a side-panel of controls. The scene now fills the entire area below the top app bar, and the per-demo controls = { ... } block is rendered inside a Material 3 ModalBottomSheet launched by a "Tune" FloatingActionButton anchored bottom-end of the scene. A semi-transparent "Settings" peek chip sits above the FAB while the sheet is closed to advertise the gesture.
- The 35 demo call-sites stay byte-identical:
DemoScaffold(title = …, controls = { … }, scene = { … })— only the placement of the controls panel has changed. - The sheet supports the partial detent (
skipPartiallyExpanded = false); drag-down, outside-tap, and back gesture all dismiss it. - AR demos keep tracking 6DOF while the sheet is at the partial detent — opening the sheet does not pause the AR session.
- Long-press the peek chip toggles
DemoSettings.qaMode(was previously a long-press on the top-app-bar title). The QA escape-hatch pill in the title bar stays unchanged. - New
DemoScaffoldTestTagsobject exposes stable testTags (demo-settings-fab,demo-settings-peek,demo-settings-sheet,demo-qa-pill) consumed byDemoInteractionTestand any future visual smoke tooling. samples/android-demo/.../DemoInteractionTest.ktlazy-opens the sheet insidetap()/tapByDesc()/dragSlider()/typeInto()when the target chip / slider isn't already visible — the 31 existing instrumentation tests work unchanged.- iOS — new
.demoSettingsSheet { … }View modifier (samples/ios-demo/SceneViewDemo/Views/Components/DemoSheet.swift) mirrors the Android pattern:.presentationDetents([.fraction(0.25), .medium, .large]),.presentationBackgroundInteraction(.enabled)so AR stays live at the partial detent, and.presentationBackground(.ultraThinMaterial)for Liquid Glass. 4 demos migrated:FogDemo,DynamicSkyDemo,MovableLightDemo(drag-anywhere gesture preserved),CameraControlsDemo(wasOrbitCameraDemo).
Visual result: scene takes ~95 % of the viewport at the default detent on Pixel 9 vs ~60 % under v4.3.0. Documented design rationale: M3 spec for bottom sheets, HIG for .sheet() with presentationDetents. Implements the plan recorded in plan_demo_settings_bottom_sheet (Stage 1 + 2 of 4 — polish + AI-first docs stages are tracked separately).
Fixed — release.yml: Dokka config-cache crash + GitHub Release decoupled from Dokka (#1150)¶
The v4.3.0 cut surfaced two latent release.yml issues that skipped the Create GitHub Release job (recovered manually):
- Dokka step now passes
--no-configuration-cache— Dokka 1.x'sdokkaSourceSetsFactoryNamedDomainObjectContainercannot be deserialized from the Gradle configuration cache, so the step crashed onrelease.ymlrun25870464897. The--retry-with-backoffwrapper from #1127 was a no-op because the error was config-cache deserialization, not a 503. Pin Dokka out of the config cache so config-cache stays enabled globally for the rest of the build. create-releasejob no longer veto-gated onpublish-api-docs— Maven Central + 3 npm packages + SPM tag are user-visible artifacts; Dokka HTML is secondary (users can still consume libraries onmvnrepository/npmwithout fresh API docs on the tag). A Dokka failure on a release tag now produces a workflow red X on the Dokka job but the GitHub Release still cuts.
Fixed — IBLPrefilter.specularFilter KDoc cost mismatch with LightEstimator (#1103)¶
The two KDocs disagreed by 10× on the same operation. Both are now accurate and cross-referenced:
IBLPrefilter.specularFilter— clarified that cost scales with cubemap face count + resolution. First-build of a 1024×1024×6 HDR skybox runs 100–200 ms (the historical figure); incremental update of a 16×16×6 ARCore cubemap (the AR path) runs 5–15 ms on a Pixel 9.LightEstimator.environmentalHdrSpecularFilter— cross-references the matrix inIBLPrefilter.specularFilterinstead of contradicting it.
Documentation-only — no behavioral change.
Fixed — GeometryDemo stacked 80 000-lux on v4.1.0 default lights (#1146)¶
Sibling of #1125 (PhysicsDemo). samples/android-demo/.../GeometryDemo.kt added a 80 000-lux directional light on top of the v4.1.0 SceneView defaults (10 000-lux main + 3 000-lux fill + IBL @ 10 000), so the metallic/roughness sweep saturated to white at every slider value. Re-tuned to 5 000 lux to match PhysicsDemo's PR #1144 retune — accent fill that complements the v4.1.0 defaults without dominating them. Acceptance #1125 only scanned for 100_000, so 80 000 slipped through; this closes the gap.
Tooling — Android CLI migration: purge legacy raw adb from install/launch paths¶
Follow-up to the May 2026 feedback_android_cli_only rule. Multiple shell scripts still drove adb install + am start directly instead of the atomic android run --apks=… --activity=… path exposed by Google's android CLI v0.7. That kept the legacy adb PATH dependency as a hard requirement and surfaced as visual-QA failures on hosts where only the android CLI was installed.
.claude/scripts/qa-android-demos.sh—--installbranch now callsandroid_cli_install_and_launch(atomic install+launch viaandroid run) with anadb install -rfallback when the CLI is missing..claude/scripts/capture-play-store-screenshots.sh— initial APK install usesandroid_cli_install_and_launchon single-device hosts; falls back toadb install -ron multi-device hosts (theandroid runsubcommand has no--deviceflag in v0.7). The per-iterationam force-stop+am start --es demo <id>block stays onadb(legit holdout —android runv0.7 has no intent-extras forwarding).tools/try-demo.sh—check_devicenow accepts eitherandroidoradbon PATH (and surfaces both install hints when neither is present). Already wired toandroid_cli_install_and_launchsince the helper landed..claude/scripts/visual-check.sh— annotated the bottom-nav tap coordinates to flag them as legitadbholdouts (no input-event API inandroidCLI v0.7).sceneview/src/androidTest/.../VisualVerificationTest.kt— KDoc now states explicitly thatadb pullis the only operation here without anandroidCLI equivalent as of v0.7.docs/docs/try.md— terminal-install snippet now showsandroid runfirst (atomic install+launch) and keepsadb install -ras the legacy alternative.
Acceptance: every legit adb holdout (no android CLI equivalent in v0.7 — pull, logcat, input tap/swipe/keyevent, am force-stop, am start --es, wait-for-device, get-state, devices, kill-server, dumpsys, pidof, uiautomator dump) is annotated in-place. Re-evaluate when android CLI v0.8+ ships any of those subcommands. No behavioural change for end users; CI render-tests.yml was already migrated by #1153.
Fixed — CI: android-demo-screenshots job unblocked + workflow validator hardened (#1153)¶
The v4.3.0 cut commit efc168bc introduced a multi-line backslash continuation in .github/workflows/render-tests.yml (the android run \\ --apks=… block under the Capture demo screenshots step). The ReactiveCircus/android-emulator-runner@v2 action exec's each line of with.script: via sh -c <line>, so the trailing \ survives as a literal argv token and android run died with Unmatched argument at index 2: '\\'. Every push to main since efc168bc failed that screenshot job, forcing chip PRs (#1145 / #1147 / #1148 / #1149) to merge with --admin and hiding any genuine screenshot regression.
- Fix: collapse the
android --no-metrics run …invocation onto a single physical line, matching the documented per-line slicing rule already followed by theattempts=0; while …; doneloop above it. - Validator extension:
.claude/scripts/check-workflow-scripts.sh(shipped by #1145) now runs a per-line slicing simulation on everywith.script:block —dash -npasses a\<EOL>because the whole-file parser splices continuations together first, but the runtime action does not. The new pass flags any trailing-backslash continuation and fails the PR check, so this class of bug can no longer ship tomainundetected. Sanity-tested by reintroducing the original break locally — validator exits1with a pointed error message. - Backwards compatibility:
run:blocks (which GitHub Actions defaults tobash -e {0}, executed as one script) are untouched; backslash continuations remain valid there. Onlywith.script:blocks (per-linesh -csemantics) are checked.
Fixed — i18n: migrate android-demo UI to stringResource(R.string.…) (#1099, closes #955)¶
PR #1073 added samples/android-demo/src/main/res/values-fr/strings.xml (164 keys) but the Compose UI never read them — every Text("…") was a hardcoded English literal, so switching the device locale to French at runtime had zero visible effect.
This PR fully closes #955 by migrating every public-facing UI surface to stringResource(R.string.…):
DemoEntrydata class refactor —title: String, subtitle: String→@StringRes titleRes: Int, @StringRes subtitleRes: Int. Thecategoryfield stays a stable non-translated key (used as map key + accent-colour lookup) with a parallelcategoryDisplayNameRes(category)helper that returns the localized header.- 37-demo registry rewritten to thread
R.string.demo_*_title/R.string.demo_*_subtitleIDs through to the Samples grid and the Explore "Try a sample" carousel. - 39 per-demo
DemoScaffold(title = "…")callsites migrated tostringResource(R.string.demo_*_title)— every demo'sTopAppBartitle now follows the active locale. - Top-level UI surfaces migrated:
RootScreen.kt(4 tab labels, About-tab 6 cards + hero tagline + footer + Star CTA),ArViewTab.kt(full launcher screen — status messages, CTA labels, featured-demo card titles, status pill, model picker, share toast, tracking-failure friendly names),DemoListScreen.kt(Samples title, "About" action, status chips, footer),DemoScaffold.kt(back-button content description),MainActivity.kt(PlaceholderDemo"Coming soon" + entry title fallback),ExploreTabScreen.kt(Explore heading, search placeholder, Animated filter chip, all carousel section titles, Categories, Recent searches, Clear, Remove $query),SketchfabModelViewerScreen.kt(Animated pill, Open-in-SceneView CTA, loading / streaming / rendered-by labels, error screen + Try again, download-failed fallback). strings.xmlexpanded from 164 → 270+ keys, covering every public-facing UI string in the priority surfaces. FRvalues-fr/strings.xmlmirrors 1-to-1.- Locale-flip verified end-to-end on Pixel_7a emulator using Android 13+ per-app locale (
adb shell cmd locale set-app-locales io.github.sceneview.demo --locales fr-FR). 4 tabs + AR launcher + Samples list + a demo AppBar all flip between EN ⇄ FR, with no regressions. Sketchfab category chips still come fromSketchfabCategories.ktand stay English — out of scope for #1099, separate larger refactor. - Existing legacy keys preserved (e.g.
demo_lighting,demo_geometry, etc.) for backwards compatibility with any external consumer holding refs to them. DeepLinkRouterTest.ktupdated to passR.string.*IDs instead of literal"Title", "Subtitle"strings — title / subtitle are not part of the route, so any pair satisfies the type.
Build green: :samples:android-demo:compileDebugKotlin + :assembleDebug + :testDebugUnitTest + :sceneview:compileReleaseKotlin + :arsceneview:compileReleaseKotlin all succeed locally.
Added — iOS parity: LightSlot + .fillLight(_:) on ARSceneView (#1138)¶
Port the second half of Android v4.3.0's #1063 (dual-light AR baseline + ENVIRONMENTAL_HDR default) to SceneViewSwift.ARSceneView. The 3D SceneView already shipped these in v4.2.0 (#1016); AR was the missing surface.
.mainLight(_:)/.fillLight(_:)modifiers onARSceneView— sameLightSlotenum as the 3DSceneView. Default.systemDefaultprovisions a10 000-lux directional main + a3 000-lux fill, matching Android'sARSceneView(mainLightNode = …, fillLightNode = …)defaults.- Reactive swap path — when the caller mutates the modifier value, the previous light's
AnchorEntityis removed fromarView.sceneand a new one is added in its place. MirrorsScene.kt:540'sprevFillLightRefdiff pattern. No full RealityView teardown. ENVIRONMENTAL_HDRparity documented —config.environmentTexturing = .automatic(already set, now annotated) is the ARKit equivalent of ARCore'sConfig.LightEstimationMode.ENVIRONMENTAL_HDR. Both drive PBR cubemap reflections for runtime-built environment probes; neither exposes a per-frame directional light estimate onfillLight.- Tests: 9 pinning tests in
ARSceneViewTests.swift(default slots, modifier copy-semantics,.disabledround-trip,.custom(LightNode)entity-identity retention, last-modifier-wins, chaining with.cameraExposure+.onSessionStarted). - Docs sync:
docs/docs/cheatsheet-ios.mdAR section + Android↔Apple mapping table;llms.txt(root +docs/docs) ARSceneView signature + LightSlot notes.
v4.3.0 — Android rendering pipeline overhaul + iOS CameraControls.pan/.firstPerson + ARRecorder + parity table (2026-05-14)¶
Status: shipped. 14-PR Android rendering audit (#1062 → #1142) hardens AR + 3D defaults, fixes 6 pre-v3 BLOCKERs (multiplicative light drift, AR IBL missing, SH coefficient swap, Box ray-parallel, spherePlaneResponse contact wrong-side, AR cubemap GEN_MIPMAPPABLE). Also closes the last #928 silent-stub item and the biggest v4.2.0 UX gap on iOS demos. PRs #1038, #1042, and the #1131–#1142 rendering + math audit batch.
Added — iOS ARRecorder record-only via ReplayKit (#1032)¶
Android has had full ARRecorder (capture + replay) since v4.0.8 via ARCore's Session.startRecording(RecordingConfig). ARKit on iOS does not expose a deterministic playback dataset, so iOS gets the record half via ReplayKit.RPScreenRecorder and replay stays Android-only.
ARRecorder@MainActorObservableObject—state: .idle / .recording / .error(message),lastOutputURL,isRecording(@Published-derived),isAvailable.async throwsAPI —startRecording() async throws,stopRecording(outputURL: URL? = nil) async throws -> URL. Bridges ReplayKit's completion-handler API to async/await.- Typed error mapping —
ARRecorderError.{permissionDenied, disabled, unavailable, alreadyRecording, notRecording, other(code:), photoLibraryDenied, photoLibrarySaveFailed}so callers can switch on the case (no string-matchingerrorDescription). ARRecorder.remembered()factory — mirrors Android'srememberARRecorder()for code-generation symmetry.ARRecorder.saveToPhotoLibrary(_:)static helper (#1043 item 2) — wrapsPHPhotoLibrary.performChangesso the recorded.movcan be copied into the user's Photos library. Mirrors Android'sARRecorder.exportToDownloads(). RequiresNSPhotoLibraryAddUsageDescriptionin the host app'sInfo.plist. Demo gets a "Save to Photos" button alongsideShareLink.- What's recorded: screen pixels only (NOT ARSession state). The
.movplays back in Photos / QuickTime; it cannot be fed back intoARSessionfor deterministic replay. UseRerunBridgefor replay-driven testing. - iOS demo:
samples/ios-demo/.../ARRecorderDemo.swiftmirrors Android'sARRecordPlaybackDemowith a record-only banner + live AR session + tap-to-place markers + "Save to Photos" +ShareLinkfor the captured.mov. Registered in the AR section ofSamplesTab. - Tests: 17 pinning tests in
ARRecorderTests.swift(state machine, error code mapping, default URL placement under.cachesDirectory/ARRecorder/, factory smoke, photo-library missing-file guard, photo-library error Equatable + localized description).
Added — CameraControls.pan + .firstPerson wired (#1034)¶
Previously, calling .cameraControls(.pan) or .cameraControls(.firstPerson) produced orbit behaviour because applyCamera() ignored the mode and pinchGesture always dollied the orbit radius. Three things shipped:
.pan: drag translates the orbittargetalong the camera-aligned right + up vectors (the scene appears to slide), pinch keeps dollying..firstPerson: drag rotates the view, no orbit translation; pinch adjusts the perspective camera'sfieldOfViewInDegrees— mirrors AndroidFovZoomCameraManipulator(range10°..120°, default60°).- Mode picker in iOS demo:
CameraControlsDemogets a 3-wayPickersegment so the v4.3.0 wiring can be felt at a glance.
New CameraControls properties: panSpeed, moveSpeed, fov, minFov, maxFov, pinchFovSpeed.
Gesture divergence from Android (documented in CameraControlMode.pan doc-comment): iOS uses 1-finger drag for pan; Android disambiguates via 2-finger strafe.
Added — Library-level auto-center content (#1026)¶
iOS demos placing content at e.g. z = -2 rendered in the bottom-third of the viewport because the default perspective camera at [0, 0.3, 2] looks at world origin. Auto-center via intermediate contentRoot entity translates user content so its centroid lands at the orbit pivot on the first frame visualBounds is non-empty (bounds query in contentRoot-local space — invariant of orbit rotation + scale). Lights stay on entities.root so they're not moved by the centring translation.
.autoCenterContent(_ enabled: Bool)modifier (defaulttrue). Passfalsefor narrative scenes with intentional off-centre placement.- iOS-only vs Android: Android achieves the same via per-demo
ModelNode(centerOrigin = Position.ZERO). Cross-platform code porting Android verbatim sees iOS re-centre implicitly; opt out for strict parity.
Added — docs/docs/cheatsheet-ios.md parity table (#1036)¶
Three-bucket reference: Deprecated on iOS (3 rows — DoF, exposure, shadowColor), Android-only / no port (4 rows — playbackDataset, SurfaceType.texture, StreetscapeGeometry, TerrainAnchor/RooftopAnchor), Approximated (3 rows — fog variants, reflection probe volumes, subsurface). Same table in llms.txt for MCP consumers.
⚠️ BREAKING — Android 3D + AR render defaults (visual)¶
SceneView and ARSceneView on Android now ship with these adjusted defaults. Apps upgrading from v4.2.0 will see visible rendering changes.
- IBL intensity (3D + AR) : Filament hardcoded ~30 000 →
DEFAULT_IBL_INTENSITY = 10 000lux (#1075, PR #1079 + PR #1088 for the AR cross-fix). Now 1:1 withDEFAULT_MAIN_LIGHT_COLOR_INTENSITY, ambient and key light contribute proportionally. Apps that hand-tunedmainLight.intensityagainst the implicit 30k IBL will see ambient drop ~3× and shadows deepen. Restore the v4.2.0 look viaindirectLight.intensity = 30_000fon your custom environment. - AR camera exposure (
ARDefaultCameraNode) : f/16 1/125 ISO 100 (EV 15, sunny-16) → f/12 1/200 ISO 200 (~1 stop brighter) (#1067 via PR #1088). Matches 3DDefaultCameraNodefor cross-mode parity and aligns with the v4.1.0 light defaults (main 10k, fill 3k). Apps that overridecameraExposure = -1.0f(the v4.0.x workaround for the sunny-16 mismatch) will now be over-exposed. Drop the override — the new defaults match. The 11 sample demos that still carried this workaround are cleaned up in CORR-A (#1101) — see "Fixed — AR rendering pipeline" below. - AR IBL specular filter default :
environmentalHdrSpecularFilter = false→true(#1064 via PR #1086). Roughness-prefilters the ARCore HDR cubemap so reflections vary visibly with material roughness instead of being mirror-like at every value. Cost : +5–15 ms / cubemap update (≈ 1 Hz from ARCore HDR mode). Restore v4.2.0 cost profile vialightEstimator.environmentalHdrSpecularFilter = false. - AR
Config.LightEstimationModedefault : ARCore's stockAMBIENT_INTENSITY→ENVIRONMENTAL_HDR(#1063 acceptance #2, CORR-A). Set insideARSceneView'ssession.configure { … }block BEFORE the user'ssessionConfigurationcallback so callers can still opt back into another mode. Front-camera sessions still forceDISABLED(ARSession.configure(...)guard, unchanged). Cost note : HDR captures + analyses the camera frame for an environmental cubemap (~1 Hz) + computes SH coefficients + main-light direction; combined with the#1064specular prefilter on the same cubemap, total cost is +5–15 ms / cubemap update. The 4 demos that previously didn't opt in (ARImageDemo,ARRooftopAnchorDemo,ARStreetscapeDemo,ARTerrainAnchorDemo) now ship HDR — all 4 are appropriate targets (1 indoor PBR helmet + 3 outdoor scenes). On HDR-unsupported devices ARCore silently degradesLightEstimate.StatetoNOT_VALIDand the#1063neutral IBL baseline stays in place — no crash, no visual regression. Restore v4.2.0 mode viasessionConfiguration = { _, c -> c.lightEstimationMode = Config.LightEstimationMode.AMBIENT_INTENSITY }. - AR
ARSceneViewtwo-light defaults (#1063 acceptance #3, CORR-A).ARSceneViewnow exposes a newfillLightNode: LightNode? = rememberFillLightNode(engine)parameter, mirroring the 3DSceneViewv4.1.0 setup (main 10k + fill 3k lux from opposite-side directional). The fill light is unaffected by ARCore light estimation — onlymainLightNodeis multiplied by the estimate. Apps that handled their own fill light + relied on the AR scene having no library-provided fill will see a brighter shadow side. Restore the v4.2.0 single-light look viaARSceneView(fillLightNode = null). DeprecatedARScenealias forwards the param. SceneView(isOpaque = false)is now actually transparent (#1077 via PR #1092). v4.2.0 ignored the flag —uiHelper.isOpaqueandview.blendModewere never wired. Apps that setisOpaque = falseand worked around the broken behaviour with custom Compose backgrounds will see double-rendering. Remove the workaround — the underlying view now bleeds through.
BREAKING-ish — silent-stub modes now active¶
Apps that called .cameraControls(.pan) or .cameraControls(.firstPerson) as effective no-ops in v4.2.0 will now see the modes do something different. To restore the v4.2.0 silent behaviour, drop the modifier (defaults to .orbit).
Apps with intentionally off-centre content will see the centroid re-centred at the orbit pivot. To restore the v4.2.0 layout, append .autoCenterContent(false).
Fixed — AR rendering pipeline (rooted in v4.0 → v4.2 regressions)¶
- 🚨 Multiplicative light drift killed
mainLightin ~15 frames (#1062 via PR #1069). Per-framemainLight.intensity *= estimate.pixelIntensitycompounded toward 0 (or ∞). Replaced by a baseline-cache pattern (compareAndSeton first valid estimate, thenbaseline * estimateeach frame). Keyed onmainLightNodeidentity so the#1017reactive swap resets cleanly. Regression pin inARMainLightBaselineMultiplyTest. - 🚨
createAREnvironmentshipped withoutIndirectLight(#1063 via PR #1069). NewiblBuffer: Buffer?parameter;rememberAREnvironmentdefaults to the bundled neutral 256×128 dim-grey IBLarsceneview/src/main/assets/neutral_environment.ibl. Metals in AR no longer render jet-black before the first ARCore estimate. - 🚨
ARSceneViewAR scene baseline now mirrors the 3DScenev4.1.0 two-light setup + opt in to ARCore real-environment estimate (#1063 acceptance criteria #2 + #3, post-#1069). Two follow-ons land in CORR-A: - New
fillLightNode: LightNode? = rememberFillLightNode(engine)parameter onARSceneView. Mirrors the 3DSceneViewv4.1.0 two-light defaults — main 10k + fill 3k lux from opposite-side directional. The fill light is unaffected by ARCore light estimation (onlymainLightNodeis multiplied by the estimate); passnullto keep a single-light AR scene. DeprecatedARScenealias forwards the new param. TheprevFillLightRefSideEffect mirrorsprevMainLightRefso reactive swaps are clean. - Default
Config.LightEstimationMode = ENVIRONMENTAL_HDR(replacing ARCore's stockAMBIENT_INTENSITY). Without HDR, the IBL baseline shipped byrememberAREnvironment(#1069) never gets replaced — PBR metals stay locked on the neutral grey baseline even after the user pans across a real scene. Set BEFORE the user'ssessionConfigurationcallback so callers can still opt back into another mode. Front-camera sessions still forceDISABLEDinsideARSession.configure(...)regardless. Documented in theARSceneViewKDoc for bothsessionConfigurationand the param section. Pinned byARCompletenessDefaultsTest(4 cases). - 🚨 SH coefficient swap on bands y20 / y21 (#1093 via PR #1100).
SPHERICAL_HARMONICS_IRRADIANCE_FACTORS[6]and[7]had swapped magnitudes and signs vs Filament's upstreamCubemapSH.cppconvention, silently producing wrong-direction matte AR shading since SceneformMaintained PR #156 (4+ years). Now matches Filament:factor[6] = +0.078848(y20),factor[7] = -0.273137(y21). 2 pinning tests added. LightEstimatordouble-closed ARCoreImageobjects in cubemap callback (#1090 via PR #1091). Theimage.use { }block already closed theImage; the trailingarImages.forEach { it.close() }then threwIllegalStateException(swallowed). Side-fix :@Volatileon 6environmentalHdr*toggles +isEnabled(#1094 via PR #1095).LightEstimatorrobustness — 3 follow-ups to #1091 / #1095 (CORR-B audit, acceptance #2 of umbrella #1094). Three latent issues that survived the first twoLightEstimatorcleanups:destroy()race vs. late render frame — added@Volatile private var isDestroyedgate at the top ofupdate()so a frame arriving afterDisposableEffect.onDisposeshort-circuits instead of touching freedengine.destroyTexturenatives.destroy()is now idempotent and latches the flag before freeing textures.- Cubemap-texture leak on
environmentalHdrReflectionstoggle — togglingtrue → falsepreviously skipped theif (reflectionsOn) { ... }branch entirely, leavingcubeMapTexture+cubeMapTextureSpecular+ the direct stagingByteBufferalive in native heap forever. New nullify-on-disable path at the top ofupdate()routes through the existing destroy-on-reassign setters; symmetric handling ofenvironmentalHdrSpecularFiltertoggling off (frees only the specular texture, preserves the base). - Staging-buffer race vs. async Filament upload — restored the
PixelBufferDescriptorcallback as a@Volatile uploadInFlightflag flip (set true beforesetImage, reset by the Filament render thread). AR thread now skips the cubemap update while in flight, preventing acubeMapBuffer.clear() + put(rgbBytes)overwrite from corrupting an in-flight GPU upload (smeared cubemap / 1-frame HDR garbage flash). Long-form comment on the callback site guards against a future refactor re-no-op'ing it. Regression suite: 14 pinning tests inLightEstimatorRobustnessTest. - AR cleanup batch — 4 follow-ups to CORR-B and post-merge audit of #1069 / #1091:
createAREnvironmentno longer advertises an inertisOpaque(#1121). The hard-codedisOpaque = truewas bypassed byskybox = null, so the parameter was effectively ignored. Dropped from the call; KDoc updated to call out that AR environments are inherently non-opaque (camera feed shows through). No behaviour change for end users.uploadInFlightcallback hoisted from per-frame allocation (#1102).Texture.PixelBufferDescriptorpreviously received a freshRunnable { uploadInFlight = false }per cubemap upload; with the new CORR-B gate firing the callback ~1 Hz, that's still one short-lived lambda per upload. Now hoisted as aprivate val uploadCompletedCallbackso a single allocation perLightEstimatorinstance covers its full lifetime. Can't move to the companion object because the callback mutates per-instance state.LightEstimatorlifecycle ownership documented (CORR-B FU-3). Class KDoc gets a new "Lifecycle ownership" section spelling out thatengineandiblPrefilterare borrowed (caller-owned, typicallyARSceneView-scoped) and the correct LIFO teardown order (estimator first, then engine).- Instrumented stress test for concurrent
update()↔destroy()(#1094 acceptance #3).LightEstimatorConcurrentDestroyTest.ktlands in bothsrc/test/(algorithmic mirror, fast CI tier — 4 tests) andsrc/androidTest/(real Filament Engine smoke — 3 tests, JNI-grounded).arsceneviewgains atestInstrumentationRunnerconfig so./gradlew :arsceneview:connectedDebugAndroidTestworks. Asserts: no exceptions, monotonicisDestroyedtransition, post-destroy textures freed, engine survives ≥10 allocate→destroy cycles.
Fixed — 3D rendering pipeline¶
- 🚨
PostProcessingDemosilently disabled SSAO on first paint (#1076 via PR #1079). Demo state initialised atfalsebut the library default istrue. Initial paint inverted the library default, hiding ambient occlusion until the user toggled it. RenderQualitypreset clobbered user view tweaks on every recomposition (#1078 via PR #1089).view.applyRenderQuality(...)was in an unkeyedSideEffect. Moved toLaunchedEffect(view, renderQuality)— preset reapplies only on actual quality change, user-setview.colorGrading/view.bloomOptionssurvive across recompositions. Switching presets still overrides preset-owned fields (intended semantic).EnvironmentLoader.createHDREnvironmentconvenience overloads silently droppedindirectLightApply(#1124). The 4 convenience overloads (asset / rawRes / file) plusloadHDREnvironment(url:)andloadKTX1Environment(url:)delegated to thebuffer:overload but forgot to forward theindirectLightApplyhook — users who wanted to override the v4.1.0-balanced 10k IBL default (#1075) had to copy the buffer-loading boilerplate. Now all overloads exposeindirectLightApply: IndirectLight.Builder.() -> Unit = {}.EnvironmentDemogains an "IBL Intensity" chip row demonstrating the override. Pinned by a Java-reflection regression test that catches any future overload that re-introduces the drop.PhysicsDemostacked 100 000 lux DIRECTIONAL on top of the v4.1.0 default lights (#1125). Pre-v4.1.0 leftover from the era when the hardcoded main light was 100k. After #1075 rebalanced main to 10k + fill to 3k + IBL to 10k, this override read 10× the new main and blew the scene out under the v4.1.0 EV ≈ 11.6 camera. Retuned to 5 000 lux as a left-side counter-fill (opposite the library's 3k right-side fill).cameraNodeleaked into sharedSceneonSceneViewunmount (#1143). SameSideEffect+AtomicReferencepattern that #1122 / PR #1131 just fixed for the main + fill lights. Switched toDisposableEffect(cameraNode) { addNode; onDispose { removeNode } }so the camera (and any HUD-space child nodes parented under it) is removed fromnodeManageron composition disposal — clean for the documented "share scene between views" use case.
Fixed — Collision math¶
- 🚨 Box ray-OBB intersection broken for parallel rays (#1096 via PR #1098).
MathHelper.MAX_DELTA = 1e-10fwas below FLT_EPSILON (~1.19e-7) for normalised ray directions, so the parallel branch never triggered —Inf / Infslab comparisons produced lottery hits on flat OBBs. New explicitabs(d) < 1e-6fparallel detection at the 3 Box slab call sites + matching twin fix inMeshCollider.AABB.rayIntersection(PR #1100). Note :MathHelper.MAX_DELTAstays at1e-10fbecause bumping it would silently breakVector3.normalized()for short vectors (documented in KDoc). - 🚨
spherePlaneResponsereturned wrong contact point on negative side (#1097 via PR #1098). Used the flipped (collision)normalfor the contact-point projection — bounce side was double-shifted off the plane. Now usesplaneNormaldirectly for the projection identitycontact = center - planeNormal * signedDist, regardless of side. Ball-on-floor no longer clips through.
Fixed — Math + collision regressions (#1126 audit batch)¶
Four sub-items audited from the sceneview-core math/animation/collision packages. Each lands as its own PR with a regression pin.
SpringAnimatorunderdamped uses analytical velocity (#1126 item 1, PR #1135). Velocity was numerically differentiated from position — produced wrong magnitude under heavy damping and integration drift at low frame rates. Now uses the closed-form analytical derivative for the underdamped case, so spring physics is frame-rate independent and correct from the first step.Quaternion.slerptransform uses exponential decay (#1126 item 2, PR #1141).Transform.slerppreviously called rawQuaternion.slerp(a, b, t)witht = deltaTime * speed, which is NOT frame-rate independent (smallertat higher fps → slower convergence). Replaced by exponential-decay formulationt = 1 - exp(-speed * deltaTime)so convergence rate is identical at 30 / 60 / 120 fps.Matrix.decomposeRotationno longer usesthisas scratch (#1126 item 3, PR #1140). The method mutatedthisas a scratch buffer during decomposition, corrupting the source matrix when callers held a reference. Two concurrent decompositions on the same matrix raced. Now allocates a local scratch —decomposeRotationis pure + thread-safe.closestPointsBetweenSegments— Ericson §5.1.9 sign (#1126 item 4, PR #1139). A sign error in the parallel-segment branch (transcribed from Christer Ericson's "Real-Time Collision Detection" §5.1.9) returned the wrong end-point pair when one segment fully shadowed the other. Now matches the reference text + 6 pinning tests for the 4 parallel-overlap topologies.
Fixed — Engine resource leaks¶
- Main + fill light add wrapped in
DisposableEffect(#1122 via PR #1131).engine.scene.addEntity(light)was called from a bareSideEffectso a removedLightNoderecomposition left the light entity attached to the Filament scene forever. Now usesDisposableEffect(mainLightNode, fillLightNode)with explicitremoveEntityon dispose — symmetric add/remove, no Filament-side leak acrossLightSlotswaps. Pinned by the existingScenelifecycle tests + a new add/remove-balance assertion. destroyMaterialsOnDisposeflag onRenderableNode+GeometryNode(#1123 via PR #1132).MaterialInstanceallocated inside a node'sapplyblock was leaked because the node assumed the material was owned by the caller. NewdestroyMaterialsOnDispose: Boolean = falseparameter (default preserves caller-owned semantics); settruewhen the node creates its ownMaterialInstance.rememberMaterialInstancehelpers default totrue, so callers using the v4.0.x recommended pattern see no leak.
Fixed — AR cubemap upload (#1142)¶
- 🚨
Texture.Buildernow setsUsage.GEN_MIPMAPPABLEfor the ARCore HDR cubemap (PR #1142). v4.3.0 RC blocker. Filament 1.71 hardened the texture-usage check andengine.createTexturenow throws when a cubemap is built withoutGEN_MIPMAPPABLEand later submitted to mipmap generation.LightEstimatorcalledtexture.generateMipmaps()immediately aftersetImage, so AR sessions withenvironmentalHdrReflections = truecrashed on the first cubemap upload (~1 second afterSTART_TRACKING). Fix adds the flag at the twoTexture.Buildercall sites + a regression pin inLightEstimatorCubemapBuilderTest.
Tooling — Bundled ARCore session recording for demos¶
samples/android-demo/src/debug/assets/ar-recordings/bundled-pixel9-sample.mp4(16 MB, debug-only sourceSet — release APK untouched, #934 protected). LetsARRecordPlaybackDemoshow a non-empty list on first launch and unblocks emulator-testable AR demos. 4 JVM tests pin the ftyp +avc1+mettcodec box layout (catches camera-only video misclassified as ARCore dataset). CI regression via the bundled recording is tracked by #1050.
Fixed — Inertia mode-gating¶
CameraControls.applyInertia() now dispatches on mode: .pan glides the target translation; .orbit and .firstPerson keep the rotation path. Previously the inertia velocity stored during a .pan drag would inject ghost rotation on release.
Fixed — Triage sweep (PR #1040)¶
- Sync-versions
--fixmode now actually rewrites SwiftPMfrom:clauses (#990). The pre-existing fix block silently no-op'd underset -euo pipefailbecause the last loop iteration's[ ] && echoshort-circuit aborted the script before reaching the rewrite. Caught by 5-agent independent review of the same PR. Coverage extended from 30 → 45 checks (13 new SwiftPMfrom:snippets across docs/website/marketing, plus rootPackage.swiftinstall snippet). DemoInteractionTestAppBar titles aligned with registry labels (#1006): Animation→Auto Rotate, Multiple Models→Multi Model, Image Node→Image Planes, Billboard Node→Billboard, Shape Node→All Shapes. The Billboard chip is nowBillboard Panelto disambiguate from the AppBar.OrbitalARDemoFloat precision drift (#978) — modulo2πon orbit + spin angles (Android + iOS) so cumulative angle survives long-running sessions.ExploreTabScreenpartial-success path (#980) —supervisorScope+catchingFeedhelper so a transient Sketchfab feed failure no longer wipes the other two;CancellationExceptionre-thrown to keep structured concurrency intact.DeepLinkRouterTest.ktJVM compile — pre-existing breakage since2556c467(4-argDemoEntryctor lost wheniconfield was added). Caught during PR #1040 5-agent review; all 13 deep-link tests now compile and run.validate-spmregex hardened (#1007) with atargets:anchor so a commented-out// .library(name: "SceneViewSwift", ...)line cannot satisfy the check.- QA script
qa_android_demos.pyupdated to the renamed registry labels.
Documented — Triage sweep¶
- Filament runtime ↔
.filamatABI invariant (#1023) inCONTRIBUTING.md: the v4.1.0 → v4.1.1 hotfix lesson, the 12 blob list, the matc recompile recipe. CLAUDE.md QUALITY RULES cross-links to it so future sessions are auto-warned.
Closed without code — Triage sweep¶
- #884 RN+Flutter version drift —
@sceneview-sdk/react-native@4.2.0andsceneview_flutter@4.2.0aligned with the monorepo on npm/pub. - #1004 iOS parity v4.2.0 umbrella — SHIPPED end-to-end; deferred items split into focused #1032 / #1033 / #1034 / #1035 / #1036.
Tests — Regression pins for the 14-PR rendering burst (CORR-C batch)¶
Pins for 5 of the 14 fixes shipped on 2026-05-14 (the highest-impact ones; remaining 7 batched for a follow-up). Each pin lives next to the fix it protects:
BoxTest.kt— 5 new methods (1 perpendicular + 4 parallel-branch on x and z axes) pinBox.rayIntersectioncorrect behaviour for thin-slab boxes. Acceptance criterion oublié de #1096.MeshColliderTest.kt— 5 new methods pin the twin parallel-ray epsilon fix inMeshCollider.AABB.rayIntersectionacross x and z axes. Acceptance criterion oublié de #1100.SceneFactoriesTest.kt(new file) — pinsDEFAULT_IBL_INTENSITY = 10_000f, the 1:1 ratio withDEFAULT_MAIN_LIGHT_COLOR_INTENSITY, and the 3DDefaultCameraNode.DEFAULT_APERTURE/SHUTTER_SPEED/ISOtriple (#1067, #1075).ARDefaultCameraNodeTest.kt(new file) — pinsARDefaultCameraNodeexposure via the new companion constants, cross-checks parity with 3DDefaultCameraNode, and asserts ≥1 stop brighter than sunny-16 (#1067). 3DDefaultCameraNodewas refactored in the same PR to expose matchingDEFAULT_APERTURE/SHUTTER_SPEED/ISOcompanion constants; AR aliases them at compile time to eliminate drift risk.RenderQualityLaunchedEffectTest.kt(new file) — pins theLaunchedEffect(view, renderQuality)re-keying contract via a 25-line JVM simulator. Pins the contract (key-equality semantics) rather than the production call site — a separate follow-up will add a Compose UI test that verifiesScene.kt:278actually keys on bothviewandrenderQuality(#1078).
CI — Batch B0 (#1116, #1117, #1118)¶
publish-api-docsnow gatescreate-release(#1116) — a Dokka build failure on a tag push now produces a workflow red X instead of a silent "Other Changes" GitHub Release with no API documentation.continue-on-error: trueand|| echoswallow removed.quality-gate.ymlskips docs-only PRs (#1117) —paths-ignoremirrors the filter already in place onci.yml. Docs PRs (typo fixes in*.md,docs/**,website-static/**,marketing/**,branding/**) no longer burn ~12 min of Android + MCP gate time.mcp*/**intentionally NOT excluded so MCP tests still run on MCP-only PRs.- Composite actions for JDK + MCP setup (#1118) — new
.github/actions/setup-gradle(JDK + Gradle cache +chmod +x ./gradlew, defaults to JDK 21, acceptsjava-version: "17"for Flutter jobs) and.github/actions/setup-mcp(Node + npm-lockfile cache +npm ciinmcp/). Adopted across 7 workflows (release, ci, pr-check, render-tests, docs, build-apks, play-store, quality-gate). Net –68 LOC, eliminates JDK-version drift, single bump point for Node/Java versions. - Render-tests sharding (#1119) filed as a follow-up —
android-library-renderiscontinue-on-error: trueand not a merge gate, so a 4× emulator boot cost vs current 20 min wall-clock needs validation before committing.
v4.2.0 — iOS parity sprint: LightSlot, RenderQuality, NodeGesture, AR anchors (2026-05-13)¶
Status: stable. Ports the v4.1.0 BREAKING render-defaults change finally to iOS, plus closes the bulk of the #928 silent-stub batch and major chunks of the iOS parity umbrella #1004.
⚠️ BREAKING — iOS render defaults match Android v4.1.0+¶
SceneView on iOS now ships with the same out-of-the-box 2-light setup that Android landed in v4.1.0:
- Main / key directional light intensity:
1 000→10 000lux (×10), pointing straight down ((0, -1, 0)). - Fill light: new
LightNode.fill(intensity: 3 000, castsShadow: false)from(0.5, -0.5, 0.5)(upper-back-left → down-front-right). 30 % of main intensity, lifts the shadow side without flattening. - Existing iOS apps will render brighter / more cinematic. To restore the v4.1.x look exactly:
Added — LightSlot / LightNode.fill / mainLight / fillLight modifiers (#1016)¶
LightSlotenum —.systemDefault/.disabled/.custom(LightNode)(3-state, exhaustive switch). Cleaner thanOptional<LightNode?>sentinel.SceneView.mainLight(_:)+SceneView.fillLight(_:)modifiers.LightNode.fill(color:intensity:castsShadow:)factory, signature-consistent withLightNode.directional(...). No baked orientation (caller calls.lookAt(_:)).@MainActor public struct LightNode— replaces the unsoundSendableconformance (LightNode wraps a non-SendableEntity).- Known limitation (#1017): light slot is read once during scene setup. Reactive replacement via
.fillLight(.custom(newLight))mid-frame is not yet wired — Android'sprevFillLightRefswap pattern (Scene.kt:287-305) needs equivalent diffing in iOSRealityView.update:.
Added — RenderQuality preset (#1018)¶
RenderQualityenum —.cinematic/.default/.performance, mirrors AndroidRenderQuality.kt.SceneView.renderQuality(_:)modifier. Walks allDirectionalLightchildren + adjustsImageBasedLightComponent.intensityExponentper tier.- iOS / Android parity gap documented in the enum doc-comment: RealityKit doesn't expose SSAO / MSAA / HDR-buffer / bloom toggles, so the iOS preset honours what's available (per-light shadow toggle + IBL intensity exponent).
Fixed — SceneView.onEntityTapped(_:) real entity hit-test (#1019, #928)¶
Previously the callback was ALWAYS called with entities.root (scene root) regardless of where the user actually tapped — useless for picking objects. Now wired via SpatialTapGesture().targetedToAnyEntity() so the callback receives the real entity at the tap location. Soft BREAKING: apps that relied on the broken behavior are unaffected (no useful logic could be built on a constant root reference).
Fixed — NodeGesture.dispatch* actually fires (#1024, #928)¶
The NodeGesture system had full registration + dispatch API surface (onTap / onDrag / onScale / onRotate / onLongPress + corresponding dispatch*) but the dispatch entry points were never CALLED from anywhere — handlers registered via entity.onTap { … } silently never fired. Wired five new .simultaneousGesture(...).targetedToAnyEntity() in SceneViewRepresentation that route to the matching NodeGesture.dispatch*. Empty-space gestures still drive the camera (existing dragGesture + pinchGesture for orbit/zoom).
Added — AR AnchorNode factories (#1025, #894 partial)¶
AnchorNode.image(group:name:)— anchor content to a detected reference image. Mirrors AndroidAugmentedImageNode.AnchorNode.face()— anchor to detected face (front-camera). Mirrors AndroidAugmentedFaceNode(pose only — no morphing-mesh; for that, drop down to rawARFaceAnchor+ custom mesh entity).AnchorNode.body()— anchor to detected human body root joint (rear-camera, iOS 13+). RealityKit-exclusive, no Android equivalent.
Fixed — AR session interruption preserves full tracking config (#1013, #928)¶
ARSceneView.Coordinator.sessionInterruptionEnded(_:) previously rebuilt ARWorldTrackingConfiguration from a single stored property (planeDetection). Image-tracking database, mesh reconstruction flag, environment-texturing setting were silently lost on every background→foreground cycle. Now the Coordinator stores + re-applies all of them.
Fixed — LightNode.spot(innerAngle:) cone-angle invariant (#1013, #928)¶
Clamps safeInner = max(0, min(innerAngle, safeOuter)) and safeOuter = max(0, min(outerAngle, π/2)). RealityKit silently produces undefined results when innerAngle > outerAngle. #if DEBUG print(...) diagnostic surfaces clamping events.
Fixed — iOS demo deep-link routing for model-viewer + multi-model (#1020, closes #1015)¶
Both ids were in DemoDeepLinkRegistry.allowedIds but had no destination(for:) cases — fell to the "Coming soon" placeholder, even though model-viewer is the App Store listing's hero screenshot. Now route to SceneGalleryDemo (the closest iOS analog to Android's tabletop multi-model scene).
Documented — CameraNode.exposure(_:) stays a deprecated no-op (#1019, negative result)¶
Investigation note: PerspectiveCameraComponent.exposureCompensation does NOT exist on RealityKit / Xcode 26.x despite an audit suggestion otherwise. Verified via direct compile failure. The deprecation now points users at the working alternatives: ARSceneView(cameraExposure:) for AR, SceneView.renderQuality(_:) to tune IBL, per-light LightNode.directional(intensity:) for the key/fill ratio.
Sample-app review¶
This release was visually validated by an Opus reviewer agent on the iPhone 16e simulator across 5 demos (lighting, geometry, animation, model-viewer, multi-model). All passed without regression. Side-finding (off-center camera framing across all iOS demos — pre-existing, not regression introduced by this release) filed as #1026.
Library API¶
| Surface | Change |
|---|---|
LightNode |
now @MainActor (was Sendable); added .fill(color:intensity:castsShadow:) factory + spot innerAngle clamp |
SceneView |
added .mainLight(_:) / .fillLight(_:) / .renderQuality(_:) modifiers; .onEntityTapped(_:) semantics fixed |
AnchorNode |
added .image(group:name:) / .face() / .body() factories |
RenderQuality |
new public enum |
LightSlot |
new public enum |
CameraNode.exposure(_:) |
improved deprecation message (still no-op on iOS — verified RealityKit-impossible) |
ARSceneView.Coordinator |
stores full tracking config across interruption |
NodeGesture |
dispatch API surface (existed already) now actually fires |
Cross-platform release set¶
sceneview / arsceneview / sceneview-core (Maven Central) + sceneview-web (npm) + @sceneview-sdk/react-native (npm) + SPM tag — all bumped to 4.2.0. sceneview-mcp continues on its independent 4.0.x patch track.
v4.1.2 — Demo app recovery: Filament .filamat mismatch fixed + AR tab no longer crashes + Samples tab redesign (2026-05-13)¶
The v4.1.0 Play Store release shipped a demo app the author summarised as "très très nul":
the AR View tab crashed the whole process on tab tap, the Samples tab was a plain 2018-era
text list, and 10 of the 24 non-AR demos consistently crashed with a libfilament-jni.so
TPanic<PostconditionPanic> SIGABRT. This release fixes all three.
Fixed — libfilament TPanic<PostconditionPanic> cascade (closes the v4.1.0 crash wave)¶
The bundled .filamat material binaries in sceneview/src/main/assets/materials/ had been
recompiled with matc 1.71 (commit efd296f1), but the Filament runtime was pinned back
to 1.70.2 (commit 4a31b579, PR #961) without recompiling the blobs. Filament 1.70.2
silently loaded the 1.71 blobs and then panicked the moment a demo bound a sampler or
uniform descriptor against the new layout — taking the whole process with it.
- Reverted the 10 sampler-bearing
.filamatto the pre-efd296f1snapshot (git checkout efd296f1~1 -- sceneview/src/main/assets/materials/). - Recompiled the two newer
opaque_unlit_colored.filamat+transparent_unlit_colored.filamatwithmatc 1.70.2from the upstreamv1.70.2release tarball so they match the runtime. - Verified on a Pixel_7a
-gpu hostemulator: 25 / 25 non-AR demos now pass (was 14 / 25 in the v4.1.0 audit). Previously crashing:lighting,movable-light,fog,environment,text,lines-paths,image,billboard,view-node,debug-overlay— all now render.
Fixed — AR View tab no longer kills the app¶
Tapping the AR View tab on v4.1.0 unconditionally instantiated a live ARSceneView. On
devices without ARCore Services installed (and on emulators) the ARCore session creation
crashed Filament with the same TPanic signature.
- New launcher screen gates the live
ARSceneViewbehind an explicit "Start AR Camera" CTA, with anArCoreApk.checkAvailability()status pill and a 2×3 grid of the six headline AR demos visible immediately. runCatchingaroundcheckAvailabilityso it can't silently die on OEMs without Play Services. CTA is hard-disabled onUNSUPPORTED_DEVICE_NOT_CAPABLE/UNKNOWN_*so the user never re-enters the panic path.- Top-right exit button on the live AR view detaches every anchor and flips back to the launcher — no more no-affordance dead end.
sessionStartedis nowrememberSaveableso process death doesn't dump users back to the launcher needlessly.
Changed — Samples tab redesign (Material 3 Expressive grid)¶
Replaces the plain ListItem text list with a 2-column M3 Expressive grid. Each card has
a compact accent-tinted icon tile (36% of card height — title and subtitle remain the
visual anchors) plus a semantic Material icon picked per demo. Categories carry distinct
accent hues (3D Basics purple, Lighting amber, Content blue, Interaction pink, Advanced
teal, AR green) so users can scan the grid by colour at a glance. Visual reference:
Sketchfab mobile + Polycam + Reality Composer launchers.
DemoEntrynow carriesicon: ImageVectorandstatus: DemoStatus(Working/KnownIssue/ComingSoon). Non-Working demos surface an outlined "Preview" / "Soon" chip with an info icon — a calm honest signal, not a red alarm.- Dark-mode accent palette (
#6446CD→#B39DDB, etc.) keeps the tinted icon tiles legible on M3 darksurfaceContainerinstead of burning at >9:1 contrast. LargeTopAppBarscroll behaviour wrapsrememberTopAppBarState()so the collapse offset survives recomposition + rotation.- Grid item keys namespaced
"demo-${id}"to guard against id collisions.
Changed — Explore tab polish¶
- Dropped the dev-flavored "Set SKETCHFAB_API_KEY (env or local.properties)" placeholder that leaked to end-user Play Store builds when the API key was missing. The Sketchfab carousels now silently fall through to the "Try a sample" carousel + categories.
SampleCardrebuilt with the same accent-tinted icon-tile layout as the Samples grid so both tabs feel like one product.FeedSectionself-hides when its Sketchfab feed is empty and not loading — no more three "Nothing here yet." headers stacked under each other in the offline path.- Dropped the red "Couldn't reach Sketchfab" banner. The empty self-hide already conveys the offline state without dev-flavored copy.
Other¶
feedback_stitch_mandatory.mdmemory rule rewritten to drop Google Stitch as the mandated UI source — reference-driven (Sketchfab mobile / Polycam / Reality Composer)DESIGN.mdtokens is the new SceneView demo workflow.- Local Sketchfab API key support in
local.propertiesfor developer builds (CI is unchanged; release builds still source the key from the GitHub Secret).
v4.1.1 — Filament 1.71.0 / .filamat ABI realignment hotfix (2026-05-12)¶
Status: stable. Critical bug fix release. All v4.1.0 consumers should upgrade.
Fixed — SIGABRT on MaterialLoader.createColorInstance (every demo using bundled materials)¶
A multi-agent post-ship audit caught a hard crash regression introduced in v4.1.0 — Lighting, Geometry, Animation, MovableLight, and MultiModel demos (and any consumer app touching MaterialLoader.createColorInstance or any default Filament post-process material) SIGABRT'd on launch with Filament: could not parse the material package for material Opaque Colored.
Root cause — Filament binary version mismatch:
- Commit
efd296f1(Apr 11) bumped Filament 1.70.2 → 1.71.0 and recompiled all 21.filamatfiles viamatc 1.71.0to material-binary version 71. - Commit
4a31b579(May 11, #961) reverted ONLYgradle/libs.versions.toml'sfilamentto1.70.2thinking the.filamatfiles were still v70 — they had been at v71 for a month. Filament 1.70.2 runtime cannot parse v71 packages →SIGABRTinlibfilament-jni.so. - v4.0.8, v4.0.9, and v4.1.0 all shipped this broken pair, but only v4.1.0 was caught (Lighting / Geometry / Animation / MovableLight / MultiModel were all new or refactored demos in the v4.1.0 sprint, exposing the regression).
The fix ([<commit-sha>]) reverts 4a31b579 — restores filament = "1.71.0" to match the v71 .filamat files. Future Filament downgrades MUST first run matc <version> against sceneview/src/main/materials/*.mat and commit the regenerated .filamats.
Tested — visual regression on Pixel_7a emulator¶
All 6 demos validated post-fix on Pixel_7a (Apple M3 host GPU, OpenGL ES 3.0):
- ✅ Lighting (was CRASH) — directional light + helmet renders correctly
- ✅ Geometry (was CRASH) — primitives render with PBR material
- ✅ Animation (was CRASH) — soldier walks in cinematic studio HDR with shadows
- ✅ MovableLight (was CRASH) — F40 model with marker sphere + intensity slider
- ✅ MultiModel (was CRASH) — 4-model tabletop tableau with studio HDR
- ✅ ModelViewer (was alive) — helmet still renders
./gradlew :sceneview:compileReleaseKotlin :arsceneview:compileReleaseKotlin :samples:android-demo:compileDebugKotlin :sceneview:test :arsceneview:testDebugUnitTest all green at Filament 1.71.0.
No public API changes¶
Library API is identical to v4.1.0. Maven Central publishes the bumped triplet (sceneview / arsceneview / sceneview-core 4.1.1) and the npm packages bump for version-tracking and to keep the cross-platform release set coherent.
v4.1.0 — iOS V1 honest + Android rendering uplift + Sketchfab streaming + Claude Code plugin marketplace (2026-05-11)¶
⚠️ BREAKING — Android render defaults change visual look out-of-the-box¶
The SceneView composable now ships with RealityKit-equivalent defaults to close the
"iOS looks better than Android" gap reviewers consistently flagged in 2026-05-10 QA:
- Main directional light intensity:
100_000→10_000lux (×10 drop). Existing apps will render noticeably darker unless they overridemainLightNode.intensityexplicitly or load a brighter IBL. Combined with shadows-now-on and a new fill light at 30% intensity, the overall scene exposure is much closer to RealityKit's defaults. - Shadows: now on by default (
setShadowingEnabled(true)). Existing apps that don't use casters will see no change; apps with floor planes will now display contact shadows. - Fill light: new
fillLightNode: LightNode?param onSceneView, defaulted torememberFillLightNode(engine). Passnullto disable for a single-light setup. - SSAO + bloom + Filmic tone mapper: now on by default on
View. SSAO has no visible cost on models without crevices; bloom strength is 0.10 (subtle, no "cheap mobile game" look). Override viaview.ambientOcclusionOptions.enabled = falseif needed. - Exposure:
setExposure(16, 1/125, 100)(sunny-16, EV~15) →(12, 1/200, 200)(neutral, EV~11.6). The previous defaults required cranking IBL intensity to see anything; the new defaults look right out of the box.
Migration: bump consumers to v4.1.0+ and review the visual delta. To restore v4.0.x
look exactly, set mainLightNode = rememberMainLightNode(engine) { intensity = 100_000f },
fillLightNode = null, and view.ambientOcclusionOptions.enabled = false.
Fixed — Android demo polish (QA pass 2026-05-11)¶
A QA agent walked the demo screens and reported user-visible papercut issues.
Five low-effort high-impact fixes shipped (65f6d8db, ea4c513e, 15c8d254, 15bcaf8c):
- ModelViewerDemo: helmet was pinned to the lower half of the viewport with a big
empty band at the top.
rememberHeroOrbitCameraManipulator(yHeight = 0.2f → 0f). - CameraControlsDemo: helmet rendered at ~10% of the viewport at the default home
camera distance.
homePosition = Position(0, 0, 4) → (0, 0, 1.5). - PhysicsDemo: first frame showed a single ball on an empty floor — the demo's hook
("colourful rain on the floor") was invisible until the user pressed Drop. Initial
sphereCount = 1 → 5so the first frame is the actual demo content. - ARStreetscapeDemo: the permission gate showed only a "Denied" error message with
no escape — Back was the only way out. Now offers
Retry(re-launches the system prompt) andOpen Settings(deep-links into the app's permission page) buttons. - DynamicSkyDemo: rendered as "fully black at noon" because
DynamicSkyNodepositions a directional sun but doesn't paint a sky dome, and the default neutral IBL had no skybox. Mitigation in the demo (not the library): swap the IBL based on the time-of-day slider —rooftop_night_2k/sunset_2k/outdoor_cloudy_2k. Three buckets is coarse but covers the obvious user expectations; a proper procedural-atmosphere skybox is library-level work for a later sprint.
Added — MovableLightDemo + OrbitalARDemo (samples)¶
Two new sample demos shipped on both iOS and Android (commits c345404b, 54233d56).
MovableLightDemo— drag-anywhere-on-the-scene → spherical-orbit math (azimuth / elevation, fixed radius 1.5 m) → light position updates live → specular highlights track the cursor on a PBR model (Damaged Helmet on Android, Ferrari F40 on iOS). Camera is locked so the only thing moving is the light; a yellow unlit marker sphere shows where the light source is. Intensity slider 1k → 100k, "Show light source" toggle hides/shows the marker.OrbitalARDemo— solar-system-style AR scene: eight distinct bundled models orbit around the user at radius 1.5 m, each with its own orbital speed (0.05 → 0.30 rad/s, 21 s to 125 s for a full lap) and a slow local spin. Heights are equipartitioned across ±0.5 m so the formation reads as varied elevations as the user turns. Plane detection is disabled — the formation lives in world space, anchored at the user's starting position.
Added — Sketchfab model viewer cross-fade (iOS + Android parity)¶
- Wow-factor hero state on the Sketchfab download screen (
1e0f86ba) — the previous bare-spinner loading state read as "loading something somewhere". Now both platforms show: (1) a Ken-Burns thumbnail (highest-res Sketchfab preview, slow 1.0→1.18 zoom, soft blur) while the GLB downloads — the screen always shows the model itself, never an empty container; (2) a ~500 ms cross-fade from thumbnail to liveSceneViewonce the model loads — the "come to life" transition that reads as proof of native rendering; (3) premiumstudio_2k.hdrIBL by default (much more flattering on PBR thanneutral_ibl, skybox kept off); (4) a 20 s hero auto-orbit so every angle is visible without touching the screen; (5) a cinematic radial vignette for the "Apple Store hero" framing. iOS uses SwiftUI.onChange(of:)+withAnimation; Android usesCrossfadefromandroidx.compose.animationkeyed on the existing Stage state machine.
Fixed — LoadingScrim on CameraControls + Animation demos (Android)¶
- First-paint black screen (
5cae550a) — QA pass on 2026-05-11 flagged "Demos noires sur first paint (Camera Controls, Lighting, Animation, Multi Model) — ~5-10s pendant lesquels l'écran est noir, user pense que l'app crash".LightingDemo+MultiModelDemoalready hadLoadingScrim; this completes the four-demo set by adding the same translucent spinner overlay toCameraControlsDemoandAnimationDemo(both load non-trivial GLBs —khronos_damaged_helmet.glb/threejs_soldier.glb— with a multi-second empty-black first-frame window).GeometryDemodeliberately skipped (procedural primitives, no model load).
Branch claude/magical-lovelace-7176b1 — staged for the next minor cut.
Added — RenderQuality preset (Android)¶
io.github.sceneview.RenderQuality(2b04c667) — one-lineCinematic/Default/Performanceswitch onSceneView. Wraps shadows, SSAO, bloom, MSAA, HDR color buffer, and dynamic resolution into three coherent presets so AI assistants generating SceneView code (or devs who don't want to learn whatambientOcclusionOptionsis) can pick one preset and ship. Individualview.*settings still win when set after the preset.rememberFillLightNode(engine)(ad81c52a) — composable factory for a secondary "fill" directional light, mirroring iOS RealityKit's default two-light setup. NewfillLightNode: LightNode?parameter onSceneViewdefaults to this; passnullto keep the single-main-light look.
Added — Sketchfab streaming scaffold¶
- iOS (
918faacd) —actor SketchfabServiceundersamples/ios-demo/.../Services/. URLSession + Codable models, on-disk LRU cache (500 MB cap), env-var-based API key (SKETCHFAB_API_KEY). - Android (
72cff080) — mirror insamples/android-demo/.../sketchfab/. OkHttp + kotlinx-serialization, same 500 MB LRU cache,BuildConfig.SKETCHFAB_API_KEYpopulated from env orlocal.properties(gitignored). - CI (
7858051f) —build-apks.ymlforwardssecrets.SKETCHFAB_API_KEYnext to the existingARCORE_API_KEYpattern. Forks / PRs from forks with an unset secret build cleanly — the gallery falls back to bundled featured models and disables Sketchfab search at runtime viaSketchfabError.MissingApiKey. - Security note — V1 scaffold bakes the key into the APK / IPA at build time. V1.1 will route through the mcp-gateway Cloudflare Worker so the master key isn't shipped; demo apps would carry only a short-lived per-user token.
TODO V1.1markers are in place inSketchfabConfig.{swift,kt}and the Gradle build script.
Changed — Android rendering defaults match iOS RealityKit¶
Closes the visible quality gap between Android (Filament) and iOS (RealityKit) out of the box. Side-by-side comparison on a Metal-backed Pixel_7a (Apple M3, -gpu host) on 5 hero models showed Android looking "blown-out / harsh" because of single-light + shadows-off + sunny-16 exposure defaults.
- Shadows on by default (
ad81c52a) —setShadowingEnabled(false → true)inSceneFactories.createView(). - Main light intensity 100 000 → 10 000 (
ad81c52a) —DEFAULT_MAIN_LIGHT_COLOR_INTENSITY. Brings it in line with RealityKit's 1 000-unit directional + IBL contribution. Crank IBL or push intensity back up explicitly when you need outdoor noon punch. - Fill light added (
ad81c52a) — secondary directional at 30% main intensity from(0.5, -0.5, 0.5), no shadows. Softens contrast on the shadow side of models. - Exposure neutralised (
ad81c52a) —setExposure(16, 1/125, 100) → (12, 1/200, 200)(~EV 15 sunny-16 → ~EV 11.6 neutral). - SSAO + bloom on (
7858051f) —view.ambientOcclusionOptions.enabled = trueandview.bloomOptions.enabled = true; strength = 0.1f. Visible grounding gain under metallic / cloth assets, invisible on plain diffuse models. Validated on toy_car / dragon / helmet / lantern / shiba. - Filmic tone mapper kept (
7858051f) — ACES was tested and produces a "cool Hollywood" grade that shifts PBR hero shots away from ground truth. SDK doesn't impose tone preferences — users opt into ACES viaview.colorGrading. (An earlier SwiftShader-based test had flagged ACES as a "PBR helmet crush" — that turned out to be a software-renderer artifact; the loss disappears on real GPU.)
ARScene.createARView() was deliberately left untouched: AR sessions have their own real-world lighting estimation, and layering SSAO / bloom on top of a camera feed is a separate sprint.
Changed — iOS V1 honest: purge the 4 silent Pareto stubs¶
Closes #928 (the 4 stubs in the Pareto-15 minimal API surface).
ModelNode.playAnimation(speed:)(141eda05) — the threeplayAnimation(...)overloads accepted aspeed: Floatparameter but never wired it through. Fixed by capturing the returnedAnimationPlaybackControllerand setting.speed = speed.CameraNode.depthOfField(focusDistance:aperture:)(141eda05) — annotated@available(*, deprecated, message: "..."). RealityKit'sPerspectiveCameraComponentdoes not expose DOF; the method is kept for Android API parity but Xcode now surfaces a clear warning.CameraNode.exposure(_:)(141eda05) — same treatment. The deprecation message redirects users toARSceneView(cameraExposure:)for AR or to scene lighting intensity for 3D.LightNode.shadowColor(_:)(141eda05) —DirectionalLightComponent.Shadowhas nocolorproperty; the parameter is ignored. Deprecation message points users atcastsShadow(_:)/shadowMaximumDistance(_:).
Added — iOS demo: "Coming soon" badges for non-ported demos¶
DemoStatusenum +ComingSoonScreen(567d6476) — Android has 37 sample demos, iOS has 16. The other 21 used to be invisible on iOS. Now they appear in theScenestab list with a "Coming v1.1" badge; tapping routes to an elegant placeholder (sablier icon, version target, links to GitHub issues + the Android demo on Play Store).- 21 placeholder items added to
SamplesTab.allScenes()covering Interaction (Camera Controls / Gesture Editing / Collision / ViewNode), Advanced extras (Post Processing / 2D Shape Extrude / Reflection Probes), Animated Model, Video Texture, and the 12 AR demos that aren't yet on iOS.
Stitch design assets (UI refonte pending)¶
Project 15993476369356042112 on Stitch contains the 8 mockup screens for the V1 UI refresh (4 iOS Liquid Glass + 4 Android M3 Expressive). Pending: actual SwiftUI / Compose implementation in samples/{ios,android}-demo based on those mockups.
Added — sceneview/claude-marketplace Claude Code plugin¶
- New marketplace repo:
github.com/sceneview/claude-marketplace(Apache-2.0). Single plugin (sceneviewv4.0.11) bundling thesceneview-mcpserver, 11 namespaced contributor commands (/sceneview:contribute,/release,/review,/test,/document,/quality-gate,/publish-check,/sync-check,/version-bump,/evaluate,/maintain), and 5 cross-platform reminder hooks that fire on edits to nudge Android ↔ iOS ↔ Web ↔ Flutter ↔ RN API parity. - Install (Claude Code):
- Marketplace clone ~256 KB (vs 1.4 GB if it had lived in the SDK monorepo — split-to-dedicated-repo decision after a multi-agent review flagged the monorepo clone as a ship-blocker).
- Plugin manifest references its npm-published MCP via
npx— no code vendoring,sceneview-mcpstays independently versioned on npm. - Discovery surfaces wired (
01114229): plugin-install instructions added toREADME.md,llms.txt,mcp/README.md,docs/docs/ai-development.md,docs/docs/index.md. GitHub topics on the marketplace repo coverclaude-code,claude-plugin,mcp,3d,ar,android,ios,web,jetpack-compose,swiftui.
Added — .claude/scripts/sync-plugin-versions.sh¶
Verifies the sceneview plugin's manifest version matches npm view sceneview-mcp version. Lives in the marketplace repo (also). Decoupled from sync-versions.sh because the plugin tracks the wrapped npm MCP, not gradle.properties VERSION_NAME.
Security — sceneview/sceneview HEAD scrub¶
Removed off-topic personal-portfolio code from the public SDK repo that had nothing to do with SceneView: hub-gateway/, hub-mcp/, mcp-gaming/, mcp-interior/, plus the strategy/registry-submission docs that listed unrelated MCPs. Also dropped tracked CDI-sensitive session artefacts (.claude/handoff*.md, .claude/plans/, .claude/marketplace-submissions/, RERUN-CHECK.md, hardcoded user paths in samples). The standard employer/portfolio identifier greps return 0 hits in HEAD. Past commits still contain the historical strings — a git filter-repo session is the planned followup.
v4.0.9 — Web unlit parity + Android demo APK -38% + Play Store race fix (2026-05-07)¶
Status: stable. No new library API surface vs v4.0.8 — instead this release bundles cross-platform unlit parity (web + Flutter + RN bridges), big Android sample-app size cuts, and a fix for the Play Store deploy workflow's recurring internal-track race.
Added — KHR_materials_unlit parity on sceneview-web¶
GeometryConfig.unlit()builder +GeometryConfig.unlit: Booleanfield on the webgeometry { … }DSL. When set, the GLB material gets the standard glTF 2.0KHR_materials_unlitextension — Filament.js supports it natively and skips PBR / IBL evaluation entirely. Closes the cross-platform unlit gap (Android already hadcreateUnlitColorInstancein v4.0.8, Apple hadCustomMaterial.unlit, RN/Flutter bridges shippedunlit: boolin v4.0.9 too).- Web demo showcase — per-shape "Unlit" checkbox in
samples/web-demoso users can A/B compare lit-PBR vs unlit on every primitive.
Added — Cross-platform unlit on bridges¶
- React Native (
react-native/) —<GeometryNode unlit={true} />exposed through the JS Fabric bridge with type-safeReadableType.Booleanparsing on the Android side (anti-crash for JS callers without strict TS). Material cache key bumped from(color)to(color, unlit)so toggling returns a fresh instance. - Flutter (
flutter/sceneview_flutter) —GeometryNode(..., unlit: true)constructor +toMap()field. API-ready for when the Android platform-view bridge gains geometry rendering (currently no-opsaddGeometry).
Performance — Android demo APK 161 MB → 100 MB (-38%)¶
- 9 orphan assets dropped (
7a466736) — 5 models (robo_bun.glb,coffee_cart.glb,koi_fish.glb,trumpet.glb,casio_keyboard.glb) + 4 environments (artist_workshop_2k.hdr,comfy_cafe_2k.hdr,pav_studio_2k.hdr,autumn_field_2k.hdr) verified unused by every sample app. Phone APK 161 → 131 MB. - TV-only assets split (
9877918e, closes #879) — moved 6 TV-exclusive models (nike_air_jordan.glb30 MB,khronos_iridescent_dish.glb,khronos_sheen_chair.glb,khronos_glam_velvet_sofa.glb,toon_cat.glb,khronos_duck.glb) from the sharedandroid-demo/assets/symlink target to a TV-demo-private folder. TV demo picks up shared assets viasourceSets.main.assets.srcDirs += '../android-demo/src/main/assets'. Phone APK 131 → 100 MB. - Disabled asset-pack module dropped (
c2fe9010) — 186 MB on-disk repo cleanup. Thesamples/android-demo-assets/com.android.asset-packmodule was disabled (assetPacks = […]commented in the demo's build.gradle) but still tracked in git. None of its 25 GLBs were referenced by code.
Fixed¶
- Play Store deploy workflow race (
f2829214) — addedmax-parallel: 1to the publish job's matrix so theinternalandproductiontracks upload sequentially. Before this, both jobs would grab the same Google Play Edit ID, one would finish first, and the other would fail with "This Edit has been deleted". Recurred on every tag push since v4.0.5; v4.0.9 deploy uses the new sequential path. - iOS demo
MARKETING_VERSIONblind spot (04e75ad5) —samples/ios-demo/SceneViewDemo.xcodeproj/project.pbxprojwas missed for 8+ releases.sync-versions.shnow covers it (29 checks, was 28).
Tested¶
NoTangentsGlbContractTest(04e75ad5) — substring"TANGENT"assertion replaced with regex anchored to theattributesblock, so a future contributor adding"comment": "no TANGENT"to the manifest cannot false-positive. Added 6th test pinning BIN chunk byte length math.TvModelListTest(9877918e) — updated to search both asset folders (TV-only + shared via sourceSets) so missing-asset regressions still fail fast.
Library API¶
No public Kotlin / Swift / Filament API changes vs v4.0.8. Maven Central artifacts are bumped for version-tracking and to keep the cross-platform release set coherent (sceneview, arsceneview, sceneview-core, sceneview-web@4.0.9, sceneview-mcp@4.0.11, SwiftPM v4.0.9, Flutter / npm bridges).
Sample-app review¶
This release was vetted by 5 parallel Opus reviewers (commit 04e75ad5) — 13 findings triaged in 4 buckets (BLOCKING / MAJOR / MINOR / NIT), all BLOCKING + MAJOR + MINOR fixed. Notable: ARFaceDemo overlay had been migrated to opaque blue in v4.0.8, hiding the user's face under a solid mask; switched back to translucent SceneViewColors.PrimaryOverlay (alpha 0.4) so the fitted face mesh actually overlays the visible face — which is the entire point of the demo.
v4.0.8 — Unlit material + 3 demo refresh + AR feature coverage (2026-05-07)¶
Status: stable. Bundles the createUnlitColorInstance material API, the AR feature coverage sprint (6 demos + ARRecorder + EIS), three demo refactors driven by on-device QA, and a regression test for the silent-closed #836 GLB-without-TANGENTS bug.
Added — Unlit colour material¶
MaterialLoader.createUnlitColorInstance(color)— flat-colour material that bypasses lighting entirely. Three overloads: FilamentColor, ComposeColor, andInt. Use for HUD overlays, gizmos, axes, lines, sprites, AR face/body meshes — anywhere PBR shading would fight the use case. Closes #871.- iOS parity:
CustomMaterial.unlit(color:)(was.debug(color:), now deprecated as alias). - Sample app migrations:
Axes3DNode,CollisionDemo,LinesPathsDemo, andARFaceDemo— the front-camera face-mesh overlay no longer needs an explicit fill light to compensate for the front-camera disablingENVIRONMENTAL_HDR. Removes a long-standing visibility-regression risk.
Changed — 3D demo refresh¶
AnimationDemo— IBL intensity slider (0–10 000 lux) replaces the hard-coded 5 000 lux baseline so users can dial atmospheric ↔ neutral. HERO orbit lifted fromyHeight = 0.15 m(low-angle monument) to0.55 m(eyes-level) so head + feet stay in frame on portrait viewports.GeometryDemo— chip row is now horizontally scrollable, all primitives spin continuously on Y, and Metallic / Roughness sliders cover the full PBR range from chalky matte (M=0, R=1) to polished mirror (M=1, R=0).MultiModelDemo— refonte from a generic spread-slider carousel to a tabletop living-room display lit bystudio_warm_2k.hdr. Front row at z=-1.3, back row at z=-1.7. Spread slider removed (the new layout is hand-tuned for the dusk-lit display).LightingDemo— 3×2.4 m backdrop wall + small coloured marker sphere at the light source so directional / point / spot read distinctly. Light pinned at (0, 1.4, 1.0) with tightened spot cone and 4 m falloff.
Fixed¶
Scene.ktcameraManipulator swap reactivity —cameraManipulatoris now wrapped inrememberUpdatedStateso the frame loop reads through a state ref. Callers that swap manipulators at runtime (e.g.AnimationDemo's scripted → Free hand-off, custom mode pickers) now seegetTransform()route to the new manipulator on the next frame instead of staying stuck on the launch-time value.
Tested¶
NoTangentsGlbContractTest(5 JVM tests) — pins the canonical "minimal lit primitive without TANGENTS" GLB binary fixture so futuregltfiobumps cannot silently break the auto-tangent synthesis path that fixes #836. Closes #863.
Added — AR feature coverage (arsceneview + samples/android-demo)¶
Five ARCore capabilities that were already wired in the library but had no demo are now showcased, plus one brand-new library feature.
ARRecorder+ARSceneView(playbackDataset = ...)— first-class ARCore Recording / Playback in SceneView.rememberARRecorder()captures the full session (camera frames, IMU, planes, depth, anchors) into an MP4;playbackDataset: File?onARSceneViewreplays that file 1:1 without a phone. Pair with the existing Rerun bridge for record-replay-inspect debugging. Library:arsceneview/src/main/java/io/github/sceneview/ar/recording/ARRecorder.kt. Demo:samples/android-demo/.../ARRecordPlaybackDemo.ktwith LIVE / RECORD / PLAYBACK modes. Recording usessetAutoStopOnPause(true)so backgrounding the app produces a clean MP4; optionalrecordingRotationkeeps replay upright across orientations.ARDepthOcclusionDemo— togglesConfig.DepthMode.AUTOMATICso real-world objects correctly hide virtual ones. Falls back to a clear "device not supported" banner whenisDepthModeSupportedreturns false. Library plumbing inARCameraStreamwas already wired.ARInstantPlacementDemo—Frame.hitTestInstantPlacement(x, y, 1.0f)places models the moment the user taps, before plane detection converges. Tracking-method badges flip from "Approximating" to "Tracked" once the trackable promotes toFULL_TRACKING.ARTerrainAnchorDemo— geospatial anchor that snaps a model to Google's terrain altitude at any lat/lng. Drop-here button gated onEarth.EarthState.ENABLEDto avoid silently swallowedIllegalStateExceptions.ARRooftopAnchorDemo— geospatial anchor that snaps to building rooftops. Same Earth-state gate as Terrain.ARImageStabilizationDemo— togglesConfig.ImageStabilizationMode.EIS. Smooths the camera background image without affecting virtual content. Gates onSession.isImageStabilizationModeSupported. Back-camera only.
llms.txt gains a new "AR Recording & Playback" section with full record + replay recipes plus a sibling "AR Image Stabilization (EIS)" section; playbackDataset appears in the ARSceneView reference signature.
Tested¶
ARRecorderTest: 21 JVM unit tests pin the Recorder state machine, error paths, andRecordingConfigbuilder calls. Surprising current behaviours pinned:stop()does not internally guard the IDLE state, andattach(newSession)mid-RECORDING is a pure pointer swap (the original session never receivesstopRecording()— see warning in the AR Recording & Playback docs).
Documented¶
docs/docs/ar-recording.md— new mkdocs page for library consumers (record + replay recipes, caveats, Rerun pairing).samples/android-demo/RECORDING_PLAYBACK.md— sample-app feature guide for demo users.README.md— new "Record & Replay AR sessions" sub-section under Developer tools.
Changed¶
ARSceneView: new optionalplaybackDataset: File? = nullparam. Snapshotted at first composition; switch playback files viakey(playbackDataset) { ARSceneView(...) }.PlaybackFailedExceptionis routed toonSessionFailed.
v4.0.7 — ARCore Cloud API key documentation everywhere + npm sceneview-mcp@4.0.9 (2026-05-06)¶
Status: stable. Documentation + MCP-server release.
Documented¶
The ARCore Cloud API key requirement (for Config.CloudAnchorMode.ENABLED,
Config.GeospatialMode.ENABLED, Config.StreetscapeGeometryMode.ENABLED) is
now surfaced everywhere a SceneView consumer might look:
arsceneview/Module.md— dedicated "ARCore Cloud API key" section in the Dokka-published lib reference (with manifest snippet + build.gradle injection- link to the setup guide).
llms.txt(root) +mcp/llms.txt+docs/docs/llms.txt— warning block under the ARSceneScope intro so AI assistants generating Cloud-using code emit the manifest/build.gradle wiring automatically.docs/docs/integrations.md— full setup section in the doc-site Cloud Anchor + Room example.mcp/src/guides.ts(returned by theget_setup_guideMCP tool): added the API_KEY meta-data + ACCESS_FINE_LOCATION permission + Cloud setup block.mcp/src/explain-api.ts(returned byexplain_api): added the missing key/permission gotcha to the "common mistakes" list.mcp/src/debug-issue.ts(returned bydebug_issue): added Cloud manifest snippet to the AR troubleshooting flow.mcp/src/samples.ts: prepended the setup comment block to the Cloud Anchor sample so generated code includes the prereq inline.samples/android-demo/STREETSCAPE_SETUP.mdshipped earlier in v4.0.6 stays the canonical step-by-step guide.
Improved — sample app demos¶
ARStreetscapeDemoandARCloudAnchorDemonow readcom.google.android.ar.API_KEYfrom the manifest at runtime (viaPackageManager.GET_META_DATA) and surface a precise "ARCore Cloud API key not configured — see STREETSCAPE_SETUP.md" banner instead of letting the user wait on "Looking for streetscape geometry…" forever or seeing a crypticERROR_NOT_AUTHORIZEDafter a tap. No-op for production builds (Play Store / App Store ship the key); helpful for forks.
Internal¶
npm sceneview-mcp4.0.8 → 4.0.9 — picks up the regeneratedmcp/src/generated/llms-txt.tssonpx sceneview-mcpusers see the new ARCore Cloud key section insceneview://api.- 8 Dependabot ip-address moderate alerts cleared via
npm audit fixacross 8 lockfiles (commita155966b). - iOS bundle 362 → 363,
MARKETING_VERSION4.0.6 → 4.0.7.
What's still in flight from v4.0.6 (unchanged)¶
- Apple TestFlight processing v4.0.6 build 362 (auto-submit pending Apple review).
- Play Store production track for v4.0.6 (Google review pending).
v4.0.6 — Streetscape Geometry / Geospatial enabled in production (2026-05-06)¶
Status: stable. Activates the AR Streetscape Geometry, Geospatial, and Cloud Anchors demos for Play Store and App Store builds. The library artefacts on Maven Central are unchanged from v4.0.5 — this release only re-builds the sample apps with the now-wired ARCore Cloud API key.
Fixed¶
The v4.0.5 sample apps shipped with com.google.android.ar.API_KEY empty in the manifest, which left the Streetscape / Geospatial / Cloud Anchors demos disabled at runtime. The wiring landed on main after v4.0.5 was tagged (commit b280b6d9 — samples/android-demo/build.gradle reads ARCORE_API_KEY from env or local.properties, injects it via manifestPlaceholders).
v4.0.6 re-cuts the sample-app AAB / iOS archive with the env var supplied by CI (secrets.ARCORE_API_KEY, restricted to package io.github.sceneview.demo + the debug, upload, and Play App Signing SHA-1s). End users of the published demos can now exercise Streetscape Geometry and Geospatial on the production builds.
Internal¶
- iOS
MARKETING_VERSION4.0.5 → 4.0.6,CURRENT_PROJECT_VERSION361 → 362 (TestFlight cumulative bundle counter). - Documentation:
samples/android-demo/STREETSCAPE_SETUP.mdshipped in v4.0.5 stays valid — provisioning a new key follows the same flow.
v4.0.5 — hotfix: android-demo compile + iOS bundle bump (2026-05-06)¶
Status: stable. Hotfix on top of v4.0.4 — that release's tag triggered Maven Central publication successfully, but the store-bound builds (Play Store APK, App Store iOS archive) failed in CI:
Fixed¶
samples/android-demo/MainActivity.kt:Unresolved reference 'initialDemo'— leftover reference to the old launch-time deep-link param after the v4.0.4 conflict resolution. Replaced with aremember { activity?.pendingDemoIdFlow?.value }capture so the NavHost picks the right start destination on first composition without re-introducing the param.samples/android-demo/demos/PhysicsDemo.kt:Assignment type mismatch: actual type is 'Node', but 'SphereNode?' was expected.— the conflict resolution wrapped the falling spheres in aNode()to attach a position via the wrapper, breakingapply = { nodeRef = this }becausethiswas the wrapper Node, not the inner SphereNode. Collapsed back toSphereNode(position = …, apply = { nodeRef = this })since SphereNode supports both.samples/ios-demo/SceneViewDemo.xcodeproj:CURRENT_PROJECT_VERSION359 → 361 — App Store Connect rejected the v4.0.4 archive (bundle version must be higher than the previously uploaded version: '360').
The v4.0.4 library artefacts on Maven Central are unchanged and still valid. v4.0.5 is intentionally minimal — only the android-demo sample app and the iOS sample app are affected.
v4.0.4 — Pixel 9 review fixes + library hardening (2026-05-06)¶
Status: stable. Brings PR #851 (87 sample-app fixes + 20 library fixes from the Pixel 9 live-review session that diverged on 2026-04-22 and never made it into v4.0.3) plus the multi-agent-review hardening of its public API surface.
Fixed — Android demo app (87 commits)¶
The store-published v4.0.3 APK shipped without the live-review fixes. v4.0.4 brings them all:
- AR demos: Face Mesh now visible (proper TANGENTS quaternion encoding via PR #852), Pose has matte materials + Blender-style axes gizmo, Streetscape falls back to plain AR when geospatial unavailable + links Google Fused Location Provider, Placement multi-model spawn + editable + Clear All, Rerun v2 UX (intro screen, live stream stats card, help dialog).
- 3D demos: Animation default Reveal+Walk + cinematic shots + dragon centred, Geometry plane no longer twisted into a wall, Physics 5×N grid spread + Drop-10 + horizontal floor + diagnostic static sphere, Lighting reactive props, DynamicSky time slider drives illumination, BillboardNode mirror, ViewNode reactive props (closes #856), Custom mesh auto-pause, MultiModel redesign, Lines/Paths 3D helix, Gesture-editing axes gizmo + sliders + live transform readout, Video Big Buck Bunny streaming + cinematic camera + creative surfaces, PostProcessing camera-orbit + SideEffect writes, Debug-overlay interactive node spawner + auto-fit + perf graph + stress test.
- Branding: launcher icons regenerated, palette sweep across collision/AR demos + Text + Billboard, gradient video, Surface base palette adoption.
- QA: deep-link --es demo <id> ingress for instrumented tests (coexists with the public scan-to-open URL routing).
Fixed — sceneview / arsceneview library (20 commits)¶
LightNode(SceneScope) now drives intensity / colour / direction reactively on recomposition (was applying only at first creation).ViewNode: reactiveposition/rotation/scale/isVisibleprops on the composable; lifecycle race on post-destroy fixed.Node/ModelNodedefaultScale(1f)regression — was(1, 0, 0)singular transform that cascaded NaN through every downstream matrix op (Physics, animations, children).MaterialInstancereassignment now propagates to all geometry nodes (Sphere/Cube/Plane).onFramecallback no longer captured stale (was ignoring recomposition).- AR camera: editable-node gestures isolated from camera gestures.
- AR
AugmentedFaceNode: tracking state callback always fires (PR #789 follow-up via PR #852). - New:
FovZoomCameraManipulator— pinch-to-FOV zoom for orthographic-style framing. - New:
DefaultCameraManipulator(pinchZoomSpeed, pinchZoomDamping)— non-linear damping curve, default tuned for dense screens (was abrupt on Pixel 9).
New — testability surface¶
- Pure-Kotlin
pinchZoomDeltaandnextFovhelpers extracted from the gesture detectors so the math curves can be regression-tested on the JVM (no Filament Engine needed). 14 new tests in:sceneview:testcover sub-pixel linearity, sign preservation, speed scaling, damping softening, FOV clamps, and default constants.
API surface — non-breaking by design¶
LightNode(color = …)parameter placed AFTERposition(not in slot 3) to preserve positional source-compat for existing 4.0.x callers passingdirectionpositionally. Documented inSceneScope.kt:354.Engine.ktsafeDestroy*helpers retainrunCatchingwrapping (the rebase-rescue PR initially stripped it; restored to avoid ABI break for v4.0.x consumers — see commit messagefd1d820e).ImageNode.destroy()deliberateTextureretention now documented in a public KDoc with the recommendedbitmap = newBitmaprecycling pattern. Tracked: #874.
Internal¶
- 14 new JVM tests (
CameraGestureMathTest). - Roborazzi screenshot tests stay
@Ignore'd (DemoListScreen renderer change tracked separately). - gradle test deps bumped: robolectric 4.14.1 → 4.16.1, roborazzi 1.43.0 → 1.60.0; new androidxTestExtJunit + androidxTestUiAutomator for instrumented coverage.
Follow-up issues filed during the rebase rescue¶
-
873: cache
SurfaceOrientationinAugmentedFaceNode.computeTangents(~30 Hz JNI alloc on hot path).¶ -
874: frame-deferred destroy queue for
ImageNode/ViewNodeGPU textures.¶
v4.0.3 — Save & Share Rerun + scan-to-open deep-links (2026-05-06)¶
Status: stable. Maven Central, Swift Package Manager, npm, and Play Store artifacts are published from this tag.
New — Rerun.io self-serve hosted viewer¶
sceneview.github.io/rerun/page added — drop a.rrdrecording on it (or paste a URL) and SceneView opens the embedded Rerun Web Viewer with the right defaults. Removes the need to install the Rerun desktop app for quick AR-debug shares (afe1cc94).RerunBridge.recordToFile(...)+share(...)(Tier-Sevents) ship on Android and iOS with full parity. iOS uses the native share sheet; Android usesMediaStore. Wire-format goldens updated (4b8993dd, fa1f8bc1).- One-command review guide
.claude/scripts/check-rerun.shfor the Save & Share MVP (58c74d3f).
New — scan-to-open deep-links¶
https://sceneview.github.io/open/?demo=<id>resolves to the published Play Store / App Store apps with the right demo pre-selected. README, website, docs all expose QR codes that route from web → installed app → specific demo (e49d4062, c95ed0d6).- Android App Links:
.well-known/assetlinks.jsonnow ships both Play App Signing and upload-key SHA-256 fingerprints — the production-signed APK is now correctly verified by Android (133df8ff). - iOS Universal Links:
SceneViewDemo.entitlementsnow declaresapplinks:sceneview.github.io(Associated Domains capability). Pairs with the existingapple-app-site-associationpublished on the website (932ac8dc).
Improved — Play Store CI (canary pattern)¶
- Push to
main→ AAB uploaded to the Play Store internal track only (snapshot for dogfooding) (12f3a5ab). - Tag
v[0-9]+.[0-9]+.[0-9]+(this release) → AAB uploaded to internal + production in parallel (canary pattern). Thev4.0.3tag triggers both jobs concurrently (1e247180). - A real release no longer requires a manual Play Console step — once green CI on the tag, the production review is auto-submitted.
Fixed — android-demo About version¶
AboutTabwas hard-coding"v4.0.0-rc.1"; now readsBuildConfig.VERSION_NAMEso the published build always shows the truthful version (f516387f).
Internal¶
- 11 commits in this release, all on
main. Tagv4.0.3is the GA cut.
v4.0.2 — Crash hardening & reactive ViewNode props (2026-05-06)¶
Status: stable. Maven Central and Swift Package Manager artifacts are published from this tag.
Fixed — Filament destroy-order crashes¶
RenderableNode.destroy()now destroys the renderable component before the entity, fixing theMaterialInstance "view" still in use by RenderableSIGABRT seen on screen navigation (#849, closes #837, #847).PlaneRenderer.destroy()routes throughMaterialLoader.destroyMaterial()to prevent double-free on AR scene teardown (#850).ViewNode.destroy()andrememberViewNodeManagerhardened against the post-destroy race that left a leakedWindowManagerview ifresume()anddestroy()interleaved within a single frame (#820, #853).
Fixed — BillboardNode mirrored texture¶
BillboardNode(andTextNodevia inheritance) no longer renders the back face of the plane quad. Switched fromlookAt(camPos)tolookTowards(worldPosition - camPos)so local +Z (front face, correct UVs) faces the viewer. Hardened guard rejects NaN inputs in addition to the zero vector (#838, #854). A 9-test JVM regression suite inBillboardNodeMathTestpins the math convention (#858).
Fixed — ViewNode reactive props¶
ViewNodecomposable restores the full reactive prop set (position,rotation,scale,isVisible) and switches fromSideEffecttoDisposableEffectkeyed on scalar components — Compose state changes now propagate without redundant per-recomposition writes (#856, #857). Closes the regression of the original7d82701cimplementation reintroduced by #842.
Security¶
honobumped to 4.12.17 acrossmcp-gateway,telemetry-workerand the bundled MCP packages — resolves thehono/jsxSSR XSS via JSX attribute names (9 alerts) (#862).postcssbumped to 8.5.14 in the same set — resolves XSS via unescaped</style>in CSS Stringify Output (4 alerts).- 0 open Dependabot alerts at the time of this entry.
Improved — Tooling¶
roborazzi1.43.0 → 1.60.0 (#830).dev.romainguy:kotlin-mathreference inllms.txtsynced to 1.8.0 across all 4 copies (root, website, well-known, bundled MCP) — AI consumers no longer suggest the outdated 1.6.0 dependency (#788 follow-up, #859).- Marketplace submission packet (OpenAI App Store + MCPize manifest) committed under
.claude/marketplace-submissions/for cross-session reuse (#855).
Internal¶
- Render tests on SwiftShader CI remain
@Ignore'd —Filament.capturePixels()still crashes the emulator. Coverage by iOS simulator, Web Playwright, and Android demo screenshot jobs. Pure-JVM math regressions can land in:sceneview:test(see #858 for the pattern).
v4.0.1 — Swift Geometry Primitives, Filament 1.71.0, Hub MCP v0.3.0¶
Status: stable. Maven Central and Swift Package Manager artifacts are published from this tag.
New — Swift Geometry Primitives¶
torus()andcapsule()added toSceneViewSwiftgeometry API, matching the Android/KMP surfaceConeNode,TorusNode,CapsuleNodedocumented in docs/nodes.md
Fixed — Filament 1.71.0 Materials¶
- Recompiled 6
.filamatmaterials for Filament 1.71.0 (closes #818) - All material binaries updated in
arsceneview/src/main/assets/
Improved — Hub MCP v0.3.0 (78 tools)¶
- 78 tools across 11 bridge-API MCPs (up from 52)
gaming-3d-mcpandinterior-design-3d-mcpfiles[]glob fix — tarball no longer ships incomplete- FREE_TOOLS count corrected (14 → 23)
Improved — Android Samples¶
- Layout and
scaleToUnitstuned across all 24 Android demo scenes for better camera framing - PhysicsDemo layout refined for Pixel 9 QA
v4.0.0 — Declarative Compose DSL, Rerun.io AR Debug, MCP Gateway & Cross-Platform Bridges¶
Status: stable. Maven Central and Swift Package Manager artifacts are published from this tag.
Backward compatible with 3.6.x. Existing code compiles and runs unchanged against 4.0.0.
New — Declarative Compose DSL (breaking rename, additive)¶
Renamed the top-level composables from Scene/ARScene to SceneView { } / ARSceneView { } across all public surfaces (KDocs, MCP packages, sample apps, docs, llms.txt, README, website). The old names are still accepted via deprecated aliases — no callers break.
- Nodes are now declared as composables inside the trailing content lambda; imperative node management is no longer the primary API.
LightNode'sapplyis a named parameter (apply = { intensity(…) }), not a trailing lambda — matches the Compose convention for layout-affecting side effects.rememberModelInstance(modelLoader, "models/file.glb")returnsnullwhile loading; all samples handle the null case explicitly.
New — AR Debug via Rerun.io¶
Stream an ARCore (Android) or ARKit (iOS) session to the Rerun viewer for scrub-and-replay debugging. Same JSON-lines wire format on both platforms, single Python sidecar handles both.
- Android: new
io.github.sceneview.ar.rerun.RerunBridge+rememberRerunBridgecomposable helper. Non-blockingDispatchers.IOscope,Channel.CONFLATEDdrop-on-backpressure, rate-limited 10 Hz by default, runtimesetEnabled()kill switch. Zero new Gradle dependencies. - iOS: new
SceneViewSwift.RerunBridge(@ObservableObjectwith@Published eventCount),Network.frameworkNWConnectionon a dedicated utility queue. NewARSceneView.onFrame { frame, arView in … }modifier — usable independently of the bridge for any per-frame custom logic. - Wire format: 5 event types (
camera_pose,plane,point_cloud,anchor,hit_result), byte-identical output from Kotlin and Swift, enforced by 24 golden-string tests (12 per platform). - Python sidecar:
tools/rerun-bridge.py— reads the TCP stream and re-logs each event as the matching Rerun archetype (Transform3D,LineStrips3D,Points3D). Spawns the Rerun viewer automatically viarr.init(spawn=True). - Playground: new "AR Debug (Rerun)" example in the
ar-spatialcategory with per-platform code tabs. - Sample apps: new
RerunDebugDemotile insamples/android-demo(Samples tab) andsamples/ios-demo(Scenes → AR category).
New — rerun-3d-mcp@1.0.0 on npm¶
New dedicated MCP server (npx rerun-3d-mcp) generating Rerun integration boilerplate from natural-language prompts. 5 tools, 73 vitest tests, Apache-2.0. Tarball 13.6 kB.
New — MCP Gateway (Cloudflare Workers + Stripe)¶
Production-grade monetization layer for sceneview-mcp:
- Cloudflare Worker (
gateway/) with Hono router, D1 database, KV namespace. - Stripe-first anonymous checkout: no login wall — user clicks CTA, pays, receives API key by email via Stripe webhook + KV single-use handoff.
- 4 plans: Free / Pro (€19) / Team (€49) / Enterprise — with tier gating and per-plan rate limiting.
POST /mcpproxy withX-Api-Keyauth, lite mode detection, and upstream routing.- Dashboard-less by design: billing managed entirely through the Stripe Customer Portal.
- 168 tests passing across gateway + hub packages.
- Live in production at
https://sceneview-mcp.mcp-tools-lab.workers.dev.
New — Anonymous telemetry worker¶
sceneview-mcp now sends lightweight anonymous usage telemetry (tool name, tier, timestamp — no personal data) to a Cloudflare Worker via batched HTTP. Sponsor CTA fires every 10 tool calls.
New — sceneview-mcp on @latest npm tag (4.0.0)¶
sceneview-mcp@4.0.0 is promoted to the @latest dist-tag. Previous @latest was 3.6.5; @next pointed to 4.0.0-rc.5. The publishConfig: { tag: "next" } guard in package.json has been removed now that the gateway go-live pipeline has verified a real paying customer.
New — Cross-platform bridges¶
- Flutter:
flutter/sceneview_flutter— PlatformView bridge to SceneView on Android + SceneViewSwift on iOS; Kotlin 2.0 + Compose Compiler plugin compatibility fixed. - React Native:
react-native/react-native-sceneview— Fabric/Turbo bridge with nativeandroid/andios/modules scaffolded. - Web:
sceneview-webKotlin/JS package (npm view sceneview-web) — Filament.js (WASM) + WebXR, webpack 5 polyfills unblocked.
New — Empire Analytics dashboard¶
website-static/ now includes a GA4-backed analytics dashboard (/analytics) for tracking playground interactions, MCP install events, and Stripe checkout funnels.
Fixes¶
- NodeAnimator (#388):
NodeAnimatornow writes animated values back to the targetNode's transform fields on every frame, fixing silent no-op animations that computed but discarded results. - Render tests (#803): Fixed intermittent SwiftShader JVM crashes in CI by sharing a single
Engineinstance per test class. The class-level@Ignoreworkarounds have been removed. - AR camera exposure (#792): Added
cameraExposureparameter toARSceneViewcomposable. - customer_creation bug:
stripe-client.tsnow guardsform.customer_creation = "always"withif (mode === "payment"), preventing a Stripe 400 error on subscription checkouts.
Tests¶
- 16 new JVM tests in
arsceneview(Rerun wire format + socket integration). - 12 new Swift tests in
SceneViewSwiftTests(cross-platform wire-format parity). - 73 new vitest tests in
mcp/packages/rerun. - 90+ new unit tests across
sceneviewandarsceneview(#814). - 168 gateway/hub tests.
Dependencies¶
- AGP bumped
8.11.1 → 8.13.2,maven-publish 0.35.0 → 0.36.0. activesupportbumped>= 7.2.3.1(CVE-2026-33176/33170/33169).
Demo apps¶
samples/android-demo: Sprint 1 refactor — 4-tab nav replaced with categorized list, 20 demos (including RerunDebugDemo).samples/android-tv-demo+samples/web-demo: broken asset refs fixed; all 8 previously-404 GLB/USDZ/HDR paths resolved.samples/ios-demo: AR Debug demo added in Scenes → AR category.
Version sweep¶
gradle.properties VERSION_NAME, all gradle.properties submodule files, npm packages, Flutter pubspec.yaml + podspec, llms.txt, docs, website, samples — synced to 4.0.0 via .claude/scripts/sync-versions.sh --fix.
v4.0.0-rc.1 — SceneView ↔ Rerun.io integration (Release Candidate)¶
Status: release candidate. Maven Central and Swift Package Manager artifacts are not published from this tag — pin to 4.0.0-rc.1 manually to test, or wait for the v4.0.0 stable tag.
Strictly additive to 3.6.2. Existing 3.6.x code compiles and runs unchanged.
New — AR Debug via Rerun.io¶
Stream an ARCore (Android) or ARKit (iOS) session to the Rerun viewer for scrub-and-replay debugging. Same JSON-lines wire format on both platforms, single Python sidecar handles both.
- Android: new
io.github.sceneview.ar.rerun.RerunBridge+rememberRerunBridgecomposable helper. Non-blockingDispatchers.IOscope,Channel.CONFLATEDdrop-on-backpressure, rate-limited 10 Hz by default, runtimesetEnabled()kill switch. Zero new Gradle dependencies. - iOS: new
SceneViewSwift.RerunBridge(@ObservableObjectwith@Published eventCount),Network.frameworkNWConnectionon a dedicated utility queue. NewARSceneView.onFrame { frame, arView in … }modifier wired to the existingARSessionDelegate.session(_:didUpdate:)— usable independently of the bridge for any per-frame custom logic. - Wire format: 5 event types (
camera_pose,plane,point_cloud,anchor,hit_result), byte-identical output from Kotlin and Swift (enforced by 24 golden-string tests, 12 per platform). - Python sidecar:
samples/android-demo/tools/rerun-bridge.py— reads the TCP stream and re-logs each event as the matching Rerun archetype (Transform3D,LineStrips3D,Points3D). Spawns the Rerun viewer automatically viarr.init(spawn=True). - Playground: new "AR Debug (Rerun)" example in the
ar-spatialcategory — embeds the official Rerun Web Viewer fromapp.rerun.ionext to the SceneView canvas with per-platform code tabs for Android / iOS / Web / Flutter / React Native / Desktop / Claude. - Sample apps: new
RerunDebugDemotile in bothsamples/android-demo(Samples tab) andsamples/ios-demo(Scenes → AR category).
New — rerun-3d-mcp@1.0.0 on npm¶
New dedicated MCP server — npx rerun-3d-mcp — that generates the Rerun integration boilerplate from natural-language prompts in any MCP client (Claude, Cursor, etc.). 5 tools:
setup_rerun_project— Gradle / SPM / Web / Python scaffolding with boilerplategenerate_ar_logger— Kotlin or Swift AR streaming helper, parameterized by data types and rategenerate_python_sidecar— TCP →rerun-sdkPython bridgeembed_web_viewer— HTML + module-script snippets for@rerun-io/web-viewerexplain_concept— focused docs forrrd,timelines,entities,archetypes,transforms
Published Apache-2.0. 73 vitest tests. Tarball size 13.6 kB (9 files).
New — sceneview-mcp@4.0.0-rc.1 on @next npm tag¶
sceneview-mcp gains the Rerun integration docs via the regenerated sceneview://api resource (82.5 kB, +5.4 kB vs 3.6.4). Stays on the @next dist-tag — @latest is intentionally pinned to 3.6.4 until the gateway go-live pipeline has a first real paying customer (see NOTICE-2026-04-11-mcp-gateway-live.md). Install the RC with npx sceneview-mcp@next.
Adds publishConfig: { tag: "next" } to mcp/package.json so future sessions can't accidentally promote the RC to @latest by running a bare npm publish.
New — AR camera exposure control (#792)¶
- Added
cameraExposureparameter toARSceneViewcomposable, allowing developers to programmatically control the camera exposure applied to the AR scene.
Fixes¶
- Render tests (#803): Fixed intermittent SwiftShader JVM crashes in CI by sharing a single
Engineinstance per test class instead of creating and tearing down one per test method. Affected classes (GeometryRenderTest,VisualVerificationTest,LightingRenderTest,RenderSmokeTest) are now stable; the class-level@Ignoreguards added as a temporary workaround have been removed. - MCP tiers test: Removed stale Polar URL from
tiers.test.tsthat was causing a test failure after the Polar → Stripe migration.
Tests¶
- 16 new JVM tests in
arsceneview(12 golden-JSON forRerunWireFormat, 4 socket integration forRerunBridgewith a mockServerSocket) - 12 new Swift tests in
SceneViewSwiftTests— identical golden strings, enforcing cross-platform wire-format parity at build time - 73 new vitest tests in
mcp/packages/rerun— 100% tool coverage - 90+ new unit tests across
sceneviewandarsceneviewmodules (#814) - Full suite validation:
./gradlew :arsceneview:compileDebugKotlin :arsceneview:testDebugUnitTest✓./gradlew :samples:android-demo:assembleDebug✓swift build --package-path SceneViewSwift✓swift test --package-path SceneViewSwift --filter Rerun*✓xcodebuild -project samples/ios-demo/SceneViewDemo.xcodeproj -scheme SceneViewDemo -destination 'generic/platform=iOS Simulator'✓
Version bump — 3.6.2 → 4.0.0-rc.1¶
Propagated to 28 files via .claude/scripts/sync-versions.sh --fix + manual touches on docs/website/samples. The 4.0.0 major bump reflects two new capabilities (Rerun integration + the 4.0.0-beta.1 gateway lite proxy shipped earlier this day by a parallel session), not breaking API changes — 3.6.x code compiles unchanged against 4.0.0-rc.1.
Release workflow¶
Git tag v4.0.0-rc.1 + GitHub pre-release created. release.yml only matches strict semver v[0-9]+.[0-9]+.[0-9]+, so this RC tag does not trigger Maven Central / SPM publish. Promote to stable by bumping to v4.0.0 and tagging again.
v3.6.2 — Cross-Platform Parity + Render Testing¶
Architecture¶
- Extract
SceneRenderer— shared render loop between SceneView and ARSceneView - Decompose
Nodegod class intoNodeGestureDelegate,NodeAnimationDelegate,NodeState - Extract
ARPermissionHandlerinterface (testable without Activity) - Fix
ModelLoader.releaseSourceData()memory leak - Clean legacy Java collision code
Quality¶
- Add 175 JVM unit tests for sceneview module
- Add 15 JVM unit tests for arsceneview module
- Add 63 KMP tests for sceneview-core
- Add 18 Swift tests for SceneViewSwift (ShapeNode)
- Fix 8 MCP test regressions
- Add pre-push quality gate script
- Stability audit: all platforms PASS
Demo Apps¶
- Rebrand to "3D & AR Explorer" (iOS + Android)
- iOS: Add model gallery, favorites, share, categorized browsing
- Android: Material 3 Expressive rewrite, 4 tabs, 40 models
- Fix Play Store build (duplicate assets in asset pack)
- Fix App Store build (private init access level)
- Fix AR camera tone mapper (rememberView → rememberARView)
Website¶
- Redesign 8 sections on homepage
- Rewrite Showcase page from scratch
- Playground: 7 platform tabs, camera manipulator, Open in Claude
- Playground: geometry primitives preview, AR placeholders
- Fix Docs 404 (redirect page)
- Auto-deploy GitHub Pages workflow
Cross-Platform¶
- iOS: Add ShapeNode (23/24 Android parity)
- iOS: Fix GeometryMaterial.custom(), ViewNode platform guard
- Web: Fix SCENEVIEW_VERSION (1.3.0 → 3.6.0)
- TV: Fix missing assets (would crash at runtime)
- MCP: Align version 3.5.5 → 3.6.0
- Flutter + React Native: Prepare for publication
- CI: Web builds now blocking, Gradle verification added
3.6.0 — Comprehensive quality audit, SwiftUI fixes, website migration (2026-03-31)¶
SceneViewSwift¶
- Fixed SceneSnapshot visionOS compilation (ARView unavailable)
- Fixed VideoNode memory leak (NotificationCenter observer never removed)
- Fixed CameraNode macOS support (removed unnecessary platform guards)
- Removed unreachable dead code in GeometryNode
Website¶
- Migrated ALL pages from model-viewer/Three.js to sceneview.js
- Removed Three.js (53K LOC) and model-viewer.min.js
- Rewrote sceneview-demo.html to use SceneView.modelViewer() API
- Fixed 3 demo pages crashing from non-existent API calls
- Fixed model paths in claude-3d.html
- Deleted 5 dead demo pages + fixed sitemap.xml
- Added 404.html page for GitHub Pages
- Fixed og:image/twitter:image meta tags (SVG → PNG) across all 8 pages
- Fixed sceneview.js version mismatch (runtime 1.5.0 → 3.6.0)
- Fixed IBL path (relative → absolute) for embed/preview subdirectory pages
- Improved synthetic IBL fallback lighting for Claude Artifacts
Branding¶
- Generated 22 PNG exports from SVG sources (logo, app icon, favicon, social, npm, store)
- Created favicon.ico (multi-resolution)
- Updated Open Collective: logo, cover, tiers (Backer $10, Sponsor $50, Gold $200), 10 tags
AI Integration¶
- Added Claude Artifacts section to llms.txt (HTML template, CDN URLs, 26 models)
- Updated MCP tool count: 22 → 26 tools, 2360 tests across 98 suites
Dependencies¶
- Bumped Filament 1.70.0 → 1.70.1
CI/CD¶
- Fixed maintenance.yml (Filament version grep, graceful fallback)
- Fixed docs.yml (download-artifact version, deploy retry)
- All 10 workflows verified green
Version alignment¶
- Updated 100+ files from 3.5.0/3.5.1 to 3.6.0
- All satellite MCPs (automotive, gaming, healthcare, interior) aligned
3.5.1 — macOS support, environment picker, MCP 3.5.3 (2026-03-29)¶
Apple platforms¶
- Native macOS support in SceneViewSwift (all source files + demo app)
- macOS App Store submission (build 357, pending review)
- iOS App Store submission (build 355, pending review)
- Environment picker UI with 6 HDR presets (Studio, Outdoor, Sunset, Night, Warm, Autumn)
- Proper macOS app icon sizes (16px to 1024px)
- Swift 6 strict concurrency fix (
@MainActoron HapticManager)
MCP Server v3.5.3¶
- Updated all dependency references from 3.4.7 to 3.5.0
- Published to npm as sceneview-mcp@3.5.3
- 1204 tests passing
CI/CD¶
- Extended app-store.yml with macOS deploy job (parallel iOS + macOS)
- Fixed TestFlight deploy failure (Swift 6 concurrency)
Documentation¶
- Added ViewNode, SceneSnapshot, SceneEnvironment.allPresets to llms.txt
- Rebuilt docs site — zero stale version references
- Fixed CDN versions in README (1.2.0 → 3.5.1) and website (1.4.0 → 3.5.1)
Assets¶
- URL-based model loading (Android + iOS)
- 6 iOS HDR environments
- Progressive texture loading (Filament async)
- 25 models migrated to GitHub Releases CDN (Play Store compliance)
3.5.0 — Full coherence audit, version alignment (2026-03-29)¶
Version coherence¶
- Unified all version references across 60+ files to 3.5.0
- Fixed module gradle.properties (sceneview, arsceneview, sceneview-core)
- Updated MCP source + dist files, docs, website, samples, Flutter, React Native
- Fixed Flutter/React Native Android build files (were still on 2.3.0)
Documentation¶
- Updated llms.txt, all docs, codelabs, cheatsheets, quickstarts
- Updated CLAUDE.md code samples and platform table
- Cross-platform version consistency across all READMEs
3.4.7 — MCP 18 tools, orbit fix, geometry demo (2026-03-26)¶
MCP Server v3.4.13¶
- 4 new tools:
get_platform_setup,migrate_code,debug_issue,generate_scene - 834 tests across all tools
Bug fixes¶
- Orbit controls: corrected inverted horizontal/vertical camera drag
- 3 core math/collision bugs fixed
- Removed stale CI job
Website¶
- Geometry demo: mini-city with 4 presets (City, Park, Abstract, Minimal)
- Meta tags, sitemap, favicon, canonical URLs polished
3.4.6 — Procedural 3D geometry in Claude Artifacts (2026-03-26)¶
Highlights¶
create_3d_artifactMCP tool with geometry type: procedural shapes with PBR materials- SceneView.js v1.1.0 published to npm: one-liner web 3D with auto Filament WASM loading
- Filament.js PBR rendering on website (replaced model-viewer)
- 9 MCP servers all at v2.0.0
3.4.5 — SceneView Web with Filament.js WASM (2026-03-26)¶
Features¶
- Real 3D rendering in browser via Google Filament compiled to WebAssembly
- 25 KB bundle (+ Filament.js from CDN)
- Live demo at sceneview.github.io
Other¶
- Website mobile polish, 50+ broken links fixed
- GitHub Sponsors: 3 new tiers; Polar.sh approved with Stripe
- MCP v3.4.9:
create_3d_artifacttool (590 tests)
3.4.4 — Play Store readiness, MCP legal (2026-03-25)¶
Features¶
- Android demo: Play Store readiness (crash prevention, dark mode, store listing)
- MCP Server: Terms of Service, Privacy Policy, disclaimers added
- GitHub Sponsors tier structure
3.4.3 — Embeddable 3D widget (2026-03-25)¶
Features¶
- Embeddable 3D viewer via single
<iframe>snippet - MCP
render_3d_previewaccepts code snippets and direct model URLs - Web demo: branded UI, model selector, loading indicator
3.4.2 — Critical AR fix, MeshNode improvement (2026-03-25)¶
Breaking fix¶
- AR materials regenerated for Filament 1.70.0 — previous materials crashed all AR apps
Features¶
MeshNodenow accepts optionalboundingBoxparameter
Security¶
- 6 Dependabot vulnerabilities fixed, 15 audit issues resolved
- 28 stale repository references updated
3.4.1 — Website, smart links, 3D preview (2026-03-25)¶
Features¶
- Website rebuilt: Kobweb replaced with static HTML/CSS/JS + model-viewer 3D
- Smart links:
/go(platform redirect),/preview(3D preview),/preview/embed(iframe viewer) - MCP
render_3d_previewtool for AI-generated 3D previews
Infrastructure¶
- 21 secrets configured (Apple + Android + Maven + npm)
- README rewritten (622 to 200 lines)
3.4.0 — Multi-platform expansion (2026-03-25)¶
New platforms¶
- Web —
sceneview-webmodule: Filament.js (WASM) rendering + WebXR AR/VR - Desktop —
samples/desktop-demo: Compose Desktop, software 3D renderer - Android TV —
samples/android-tv-demo: D-pad controls, model cycling - Flutter —
samples/flutter-demo: PlatformView bridge (Android + iOS) - React Native —
samples/react-native-demo: Fabric bridge (Android + iOS)
Android showcase¶
- Unified
samples/android-demo— Material 3 Expressive, 4 tabs, 14 demos - Blue branding with isometric cube icon
Infrastructure¶
- MCP Registry — SceneView MCP published at
io.github.sceneview/mcp - 21 GitHub Secrets — Android + iOS + Maven + npm fully configured
- Apple Developer — Distribution certificate, provisioning profile, API key
- CI/CD — Play Store + App Store workflows ready
Samples cleanup¶
- 15 obsolete samples deleted, merged into unified platform demos
{platform}-demonaming convention across all 7 platforms- Code recipes preserved in
samples/recipes/
Fixes¶
- material-icons-extended pinned to 1.7.8 (1.10.5 not published on Google Maven)
- wasmJs target disabled (kotlin-math lacks WASM variant)
- AR emulator script updated for new sample structure
3.3.0 — Unified versioning, cross-platform, website¶
Version unification¶
- All modules aligned to 3.3.0 — sceneview, arsceneview, sceneview-core, MCP server, SceneViewSwift, docs, and all references across the repo are now at a single unified version
SceneViewSwift (Apple)¶
- iOS 17+ / macOS 14+ / visionOS 1+ via RealityKit — alpha
- Node types: ModelNode, AnchorNode, GeometryNode, LightNode, CameraNode, ImageNode, VideoNode, PhysicsNode, AugmentedImageNode
- PBR material system with textures
- Swift Package Manager distribution
SceneViewSwift — new nodes and enhancements¶
- DynamicSkyNode — procedural time-of-day sky with sun position, atmospheric scattering
- FogNode — volumetric fog with density, color, and distance falloff
- ReflectionProbeNode — local cubemap reflections for realistic environment lighting
- ModelNode enhancements — named animation playback, runtime material swapping, collision shapes
- LightNode enhancements — shadow configuration, attenuation radius and falloff
- CameraNode enhancements — field of view, depth of field, exposure control
MCP server — iOS support¶
- 8 Swift sample snippets for iOS code generation
get_ios_setuptool for Swift/iOS project bootstrapping- Swift code validation in
validate_codetool - iOS-specific guides and documentation
Tests¶
- 65+ new tests covering edge cases and platform-specific behavior
- Test coverage for all 15+ SceneViewSwift node types
- Platform tests for iOS-specific RealityKit integration
Website¶
- Platform logo ticker on homepage — infinite-scroll marquee showing all supported platforms and technologies (Android, iOS, macOS, visionOS, Compose, SwiftUI, Filament, RealityKit, ARCore, ARKit, Kotlin, Swift)
- CSS-only animation with fade edges, hover-to-pause, dark mode support
Documentation¶
- Updated ROADMAP.md to reflect current state (SceneViewSwift exists, phased plan revised)
- Updated PLATFORM_STRATEGY.md — native renderer per platform architecture (Filament + RealityKit)
- All codelabs, cheatsheet, migration guide updated to 3.3.0
- iOS quickstart guide — step-by-step setup for SceneViewSwift
- iOS cheatsheet — quick reference for SwiftUI 3D/AR patterns
- 2 SwiftUI codelabs — hands-on tutorials for iOS 3D scenes and AR
3.1.2 — Sample polish, CI fixes, maintenance tooling¶
Fixes¶
autopilot-demo: remove deprecatedengineparameter fromPlaneNode,CubeNode,CylinderNodeconstructors (API aligned with composable node design)- CI: fix AR emulator stability — wait for launcher, dismiss ANR dialogs, kill Pixel Launcher before screenshots
Sample improvements¶
model-viewer: scale up Damaged Helmet 0.25 → 1.0; add Fox model (CC0, KhronosGroup glTF-Sample-Assets) with model picker chip rowcamera-manipulator: scale up model 0.25 → 1.0; add gesture hint bar (Drag·Orbit / Pinch·Zoom / Pan·Move)
Developer tooling¶
/maintainClaude Code skill + daily maintenance GitHub Action for automated SDK upkeep- AR emulator CI job using x86_64 Linux + ARCore emulator APK for screenshot verification
ROADMAP.mdadded covering 3.2–4.0 milestones
3.1.1 — Build compatibility patch¶
- Downgrade AGP from 8.13.2 → 8.11.1 for Android Studio compatibility
- Update AGP classpath in root
build.gradleto match - Refresh
gltf-camerasample: animated BrainStem character + futuristic rooftop night environment
3.1.0 — VideoNode, reactive animation API¶
New features¶
VideoNode— render a video stream (MediaPlayer / ExoPlayer) as a textured 3D surface- Reactive animation API — drive node animations from Compose state
ViewNoderename —ViewNode2unified intoViewNode
Fixes¶
ToneMapper.LinearinARSceneprevents overlit camera backgroundImageNodeSIGABRT: destroyMaterialInstancebefore texture on disposecameraNoderegistered withSceneNodeManagerso HUD-parented nodes render correctly- Entities removed from scene before destroy to prevent SIGABRT
UiHelperAPI corrected for Filament 1.56.0
AI tooling¶
- MCP server:
validate_code,list_samples,get_migration_guidetools + live Issues resource - 89 unit tests for MCP validator, samples, migration guide, and issues modules
3.0.0 — Compose-native rewrite¶
Breaking changes¶
The entire public API has been redesigned around Jetpack Compose. There is no source-compatible upgrade path from 2.x; see the Migration guide for a step-by-step walkthrough.
Scene and ARScene — new DSL-first signature¶
Nodes are no longer passed as a list. They are declared as composable functions inside a trailing content block:
// 2.x
Scene(
childNodes = rememberNodes {
add(ModelNode(modelInstance = loader.createModelInstance("helmet.glb")))
}
)
// 3.0
Scene {
rememberModelInstance(modelLoader, "models/helmet.glb")?.let { instance ->
ModelNode(modelInstance = instance, scaleToUnits = 1.0f)
}
}
SceneScope — new composable DSL¶
All node types (ModelNode, LightNode, CubeNode, SphereNode, CylinderNode, PlaneNode,
ImageNode, ViewNode, MeshNode, Node) are now @Composable functions inside SceneScope.
Child nodes are declared in a NodeScope trailing lambda, matching how Compose UI nesting works.
ARSceneScope — new AR composable DSL¶
All AR node types (AnchorNode, PoseNode, HitResultNode, AugmentedImageNode,
AugmentedFaceNode, CloudAnchorNode, TrackableNode, StreetscapeGeometryNode) are now
@Composable functions inside ARSceneScope.
rememberModelInstance — async, null-while-loading¶
// Returns null while loading; recomposes with the instance when ready
val instance = rememberModelInstance(modelLoader, "models/helmet.glb")
SurfaceType — new enum¶
Replaces the previous boolean flag. Controls whether the 3D surface renders behind Compose layers
(SurfaceType.Surface, SurfaceView) or inline (SurfaceType.TextureSurface, TextureView).
PlaneVisualizer — converted to Kotlin¶
PlaneVisualizer.java has been removed. PlaneVisualizer.kt replaces it.
Removed classes¶
The following legacy Java/Sceneform classes have been removed from the public API:
- All classes under
com.google.ar.sceneform.*— replaced by Kotlin equivalents under the same package path (.ktfiles). - All classes under
io.github.sceneview.collision.*— replaced by Kotlin equivalents. - All classes under
io.github.sceneview.animation.*— replaced by Kotlin equivalents.
Samples restructured¶
All samples are now pure ComponentActivity + setContent { }. Fragment-based layouts have been
removed. The model-viewer-compose, camera-manipulator-compose, and ar-model-viewer-compose
modules have been merged into model-viewer, camera-manipulator, and ar-model-viewer
respectively.
Bug fixes¶
ModelNode.isEditable—SideEffectwas resettingisEditableto the parameter default (false) on every recomposition, silently disabling gestures whenisEditable = truewas set only insideapply { }. PassisEditable = trueas a named parameter to maintain it correctly.- ARCore install dialog — Removed
canBeInstalled()pre-check that threwUnavailableDeviceNotCompatibleExceptionbeforerequestInstall()was called, preventing the ARCore install prompt from ever appearing on fresh devices. - Camera background black —
ARCameraStreamusedRenderableManager.Builder(4)with only 1 geometry primitive defined (invalid in Filament). Fixed toBuilder(1). - Camera stream recreated on every recomposition —
rememberARCameraStreamused a default lambda parameter as arememberkey; lambdas produce a new instance on every call, making the key unstable. Fixed by keying onmaterialLoaderonly. - Render loop stale camera stream — The render-loop coroutine captured
cameraStreamat launch; recomposition could recreate the stream while the loop kept updating the old (destroyed) one. Fixed with anAtomicReferenceupdated viaSideEffect.
New features¶
SceneScope/ARSceneScope— fully declarative, reactive 3D/AR content DSLNodeScope— nested child nodes using Compose's natural trailing lambda patternSceneNodeManager— internal bridge that syncs Compose snapshot state with the Filament scene graph, enabling reactive updates without manualaddChildNode/removeChildNodecallsSurfaceType— explicit surface-type selection (SurfacevsTextureSurface)ViewNode— Compose UI content rendered as a 3D plane surface in the sceneEngine.drainFramePipeline()— consolidated fence-drain extension for surface resize/destroyrememberViewNodeManager()— lifecycle-safe window manager forViewNodecomposables- Autopilot Demo — new sample demonstrating autonomous animation and scene composition
- Camera Manipulator — new dedicated sample for orbit/pan/zoom camera control
Node.scaleGestureSensitivity— newFloatproperty (default0.5) that damps pinch-to-scale gestures. Applied as1f + (rawFactor − 1f) × sensitivityinonScale, making scaling feel progressive without reducing the reachable scale range. Set it per-node in theapplyblock alongsideeditableScaleRange.- AR Model Viewer sample — redesigned with animated scanning reticle (corner brackets +
pulsing ring), model picker (Helmet / Rabbit), auto-dismissing gesture hints,
enableEdgeToEdge(), and a clean Material 3 UI.
2.3.0¶
- AGP 8.9.1
- Filament 1.56.0 / ARCore 1.48.0
- Documentation improvements
- Camera Manipulator sample renamed