Skip to content

fix(ar): flip environmentalHdrSpecularFilter default to true (#1064)#1086

Merged
thomas-gorisse merged 1 commit into
mainfrom
claude/issue-1064-specular-filter-default
May 14, 2026
Merged

fix(ar): flip environmentalHdrSpecularFilter default to true (#1064)#1086
thomas-gorisse merged 1 commit into
mainfrom
claude/issue-1064-specular-filter-default

Conversation

@thomas-gorisse
Copy link
Copy Markdown
Contributor

Closes #1064.

The toggle exists, just defaulted wrong. Filament's IndirectLight.reflections() expects a roughness-prefiltered mip chain — feeding it the raw ARCore cubemap means every roughness lookup samples mip 0 → mirror-like reflections regardless of the material's roughness slider, oversaturated highlights, color shifts on metallic/glossy models.

Flip the default to true (the toggle's existence already documented this is the right behavior — only the default was wrong).

Cost: one-time prefilter pass per cubemap update (~5–15 ms on Pixel 9). ARCore updates the HDR cubemap ~1 Hz → net AR frame time impact under 0.5 ms.

QA

  • :arsceneview:compileReleaseKotlin green
  • :arsceneview:testDebugUnitTest 11 LightEstimatorTest + 3 ARMainLightBaselineMultiplyTest green
  • Visual: open model-viewer-ar with the helmet, verify reflections vary visibly with the roughness slider (pre-fix: identical mirror-like at all values)

🤖 Generated with Claude Code

Closes #1064. Filament's `IndirectLight.reflections()` expects a
roughness-prefiltered mip chain. Feeding it the raw ARCore cubemap means
every roughness lookup samples mip 0 → mirror-like reflections regardless
of the material's roughness slider, oversaturated highlights, color shifts
on metallic/glossy models.

The toggle existed (`environmentalHdrSpecularFilter`) but defaulted to
`false`. Flip the default to `true` so the prefilter actually runs out of
the box. Power users who want to opt out (unlit-only scenes, perf budget)
can still set `false` explicitly.

Cost: one-time prefilter pass per cubemap update (~5–15 ms on Pixel 9).
ARCore updates the HDR cubemap roughly once per second so the cost is
amortised; net AR frame time impact under 0.5 ms.

Updated the KDoc with rationale + costs (the previous one just described
specular highlights generically without explaining when to flip the toggle).

Verified:
- :arsceneview:compileReleaseKotlin green
- :arsceneview:testDebugUnitTest 11+3 tests green (LightEstimatorTest +
  ARMainLightBaselineMultiplyTest from #1062)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thomas-gorisse thomas-gorisse merged commit cf34f10 into main May 14, 2026
@thomas-gorisse thomas-gorisse deleted the claude/issue-1064-specular-filter-default branch May 14, 2026 01:33
thomas-gorisse added a commit that referenced this pull request May 14, 2026
…cker) (#1142)

CORR-D visual QA on Pixel 9 (Android 16) caught SIGABRT on EVERY AR demo
launch since PR #1086 flipped `environmentalHdrSpecularFilter` default to
`true`:

  Filament: Precondition in generateMipmaps:767
  reason: Texture usage does not have GEN_MIPMAPPABLE set
  Fatal signal 6 (SIGABRT)

Root cause: `iblPrefilter.specularFilter()` (called when
`environmentalHdrSpecularFilter == true`) internally invokes
`Texture.generateMipmaps()`. Filament 1.70+ asserts on this if the
texture wasn't built with `Texture.Usage.GEN_MIPMAPPABLE`. Pre-#1086 the
prefilter was off by default so the missing flag was latent.

The fix is the exact pattern already used in `ImageTexture.kt:23-26` for
the same reason:

    .usage(Texture.Usage.DEFAULT or Texture.Usage.GEN_MIPMAPPABLE)

Single 2-line addition to the `Texture.Builder()` chain at line 343-349.
Compile + arsceneview unit tests green.

This is a v4.3.0 cut blocker — without it, AR is broken in production
for every consumer on every device with ARCore ENVIRONMENTAL_HDR mode.

Refs #1106 (visual QA umbrella, CORR-D report), #1086 (the spec filter
default flip that exposed the latent bug).

Co-authored-by: Thomas Gorisse <thomas.gorisse@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thomas-gorisse added a commit that referenced this pull request May 14, 2026
…ng PRs — BLOCKER found (#1149)

Umbrella #1106 visual QA on Pixel 9 (Android 16). Result:

- 25/25 non-AR demos PASS via deep-link smoke test (`/tmp/qa-smoke.sh`).
- 4/5 applicable targeted checks PASS (PostProcessing SSAO toggle #1076,
  Environment Gallery IBL 10k #1075, Physics sphere-on-floor #1097,
  implicit SH coefficients via env cycle #1093).
- 2 BLOCKING crashes on AR demos: `ARPlacement` + `AROrbital` both SIGABRT
  with `Filament: generateMipmaps — Texture usage does not have GEN_MIPMAPPABLE set`.

Root cause analysed in REPORT.md: PR #1086 flipped
`environmentalHdrSpecularFilter` default to `true`, which routes the AR HDR
cubemap through `iblPrefilter.specularFilter()` → Filament `generateMipmaps()`.
The cubemap is built at `LightEstimator.kt:234-241` without
`Texture.Usage.GEN_MIPMAPPABLE`, so the precondition fires on the first
cubemap update. Fix proposals (Option A: add the usage flag; Option B:
revert default to `false`) are documented in REPORT.md.

Verdict: BLOCKING-REGRESSION-FOUND — v4.3.0 must not cut until fixed.

Also patches `.claude/scripts/qa-android-demos.sh` line 205 (set -euo pipefail
+ grep no-match was killing the loop after 2 demos) and unignores the dated
audit folder under `tools/qa-screenshots/`.

Co-authored-by: Thomas Gorisse <thomas.gorisse@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thomas-gorisse added a commit that referenced this pull request May 14, 2026
…de leak (#1124 / #1125 / #1143) (#1147)

* fix(ar): AR cubemap Texture.Builder needs GEN_MIPMAPPABLE (v4.3.0 blocker)

CORR-D visual QA on Pixel 9 (Android 16) caught SIGABRT on EVERY AR demo
launch since PR #1086 flipped `environmentalHdrSpecularFilter` default to
`true`:

  Filament: Precondition in generateMipmaps:767
  reason: Texture usage does not have GEN_MIPMAPPABLE set
  Fatal signal 6 (SIGABRT)

Root cause: `iblPrefilter.specularFilter()` (called when
`environmentalHdrSpecularFilter == true`) internally invokes
`Texture.generateMipmaps()`. Filament 1.70+ asserts on this if the
texture wasn't built with `Texture.Usage.GEN_MIPMAPPABLE`. Pre-#1086 the
prefilter was off by default so the missing flag was latent.

The fix is the exact pattern already used in `ImageTexture.kt:23-26` for
the same reason:

    .usage(Texture.Usage.DEFAULT or Texture.Usage.GEN_MIPMAPPABLE)

Single 2-line addition to the `Texture.Builder()` chain at line 343-349.
Compile + arsceneview unit tests green.

This is a v4.3.0 cut blocker — without it, AR is broken in production
for every consumer on every device with ARCore ENVIRONMENTAL_HDR mode.

Refs #1106 (visual QA umbrella, CORR-D report), #1086 (the spec filter
default flip that exposed the latent bug).

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

* fix(env): EnvironmentLoader convenience overloads forward indirectLightApply (#1124)

The 4 convenience overloads of createHDREnvironment (asset / rawRes / file) and
the suspend loadHDREnvironment(url) silently dropped indirectLightApply on the
way down to the buffer overload. Users wanting to override the v4.1.0-balanced
10k IBL default (#1075) — e.g. bright outdoor HDRIs that look better at 30k+ —
had to copy the buffer-loading boilerplate to get the hook.

- Add `indirectLightApply: IndirectLight.Builder.() -> Unit = {}` to the asset,
  rawResId, file, url HDR overloads + the loadKTX1Environment(url:) variant
  that internally routes through createHDREnvironment.
- Update EnvironmentDemo (the closest thing to an Environment Gallery) with an
  "IBL Intensity" chip row (Default 10k / Bright 30k / Dim 3k) so the override
  path is exercised in the showcase app.
- Reflection-based JVM regression test pins the API contract — any future
  overload that drops the lambda fails the contract test without needing
  kotlin-reflect on the test classpath or a live Filament Engine.

Closes #1124

* fix(demo): PhysicsDemo no longer stacks 100k lux on v4.1.0 default lights (#1125)

PhysicsDemo's explicit DIRECTIONAL light was set to 100_000 lux — a pre-v4.1.0
override that made sense when the library's hardcoded main light was 100k, but
became a 10× over-exposure once #1075 rebalanced main to 10k + fill to 3k +
IBL to 10k. Total scene was 123k under the v4.1.0 EV ≈ 11.6 default camera,
blowing out the floor and the colored spheres.

Retune to 5_000 lux — a soft accent fill that complements the v4.1.0 defaults
without dominating them. Comment now documents the rationale so the next
default rebalance doesn't reintroduce the leftover.

Closes #1125

* fix(scene): wrap cameraNode add in DisposableEffect (#1143)

Same leak shape as the #1122 lights fix (PR #1131): the cameraNode was added
via SideEffect + prevCameraNodeRef AtomicReference, which only triggered the
removal on a *different-node* recomposition. A SceneView leaving composition
cleanly orphaned the camera (and any HUD-space child nodes parented under it)
in the underlying Filament Scene — a cumulative leak when sharing a single
Scene between multiple SceneView composables (the use case documented at
Scene.kt:191).

Switch to `DisposableEffect(cameraNode) { addNode; onDispose { removeNode } }`
— exact same pattern as the main/fill light fix. The `prevCameraNodeRef` is
now dead code and removed.

Closes #1143

* chore(bug-wave): apply 5-reviewer FIX-MERGE feedback

- Test: tighten `hasBuilderLambdaParam()` to pin the IndirectLight.Builder
  generic on the Function1 (R4 MAJOR). The previous redundant double-check
  for any `Function1` would have green-lit a future unrelated `(Float)->Unit`.
- KDoc: add `@param indirectLightApply` to the buffer overload of
  `createHDREnvironment` for parity with the 4 convenience overloads (R5).
- Demo comment: fix "front-right" → "left-side counter-fill" (the direction
  `(-0.3, -1, -0.5)` points front-LEFT; the new light is opposite the
  library's default 3k right-side fill) (R2).
- llms.txt + docs/docs/llms.txt: list the 5 HDR overloads and surface the
  new `indirectLightApply` hook so MCP/AI consumers know it exists (R5).
- CHANGELOG: 3 entries under v4.3.0 UNRELEASED "Fixed — 3D rendering
  pipeline" for #1124 / #1125 / #1143 (R5 MAJOR).

---------

Co-authored-by: Thomas Gorisse <thomas.gorisse@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thomas-gorisse added a commit that referenced this pull request May 14, 2026
…#1197)

Pixel 9 v4.3.0 production audit (umbrella #1176) hotfix follow-up. The umbrella's
P0/P1 code bugs all landed by v4.3.2 (PRs #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 needs a Google-Cloud-Console-side change to fully unblock end users.

Changes:

- ARCloudAnchorDemo.kt: When `host()` / `resolve()` returns ERROR_NOT_AUTHORIZED,
  the 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, so production users see actionable guidance. (#1177)

- samples/android-demo/STREETSCAPE_SETUP.md: New "Play App Signing key
  (production blocker — issue #1177)" section. Documents the runbook to add the
  Play App Signing SHA-1 (post-resign, NOT the upload key) to the Cloud API
  key restrictions. Propagation is ~1 min; no new AAB cut needed once Play
  App Signing key SHA-1 is whitelisted.

- .claude/scripts/verify-arcore-key.sh: New CI guard, wired into play-store.yml.
  Fails fast if ARCORE_API_KEY secret is missing, if build.gradle no longer
  injects the arcoreApiKey manifest placeholder, or if AndroidManifest.xml drops
  the ${arcoreApiKey} reference. Catches the silent-regression class that ships
  an AAB with an unwired Cloud key.

Verified-fixed closures (issues stay open just for tracker hygiene — code
already lives on main):

- #1097 spherePlaneResponse wrong contact point on negative side — fixed in
  CollisionResponse.kt line 89 (contactPoint = center - planeNormal * signedDist,
  projects along the original unflipped normal). JVM regression test
  `spherePlaneResponseContactPointLandsOnPlaneOnEitherSide` pins it.

- #1178 AR Terrain & Rooftop Anchors fail under R8 — fixed in PR #1190 already
  on main. Consumer rules keep com.google.android.gms.location.**,
  common.api.**, tasks.**. Verified by dexdump on this hotfix's
  assembleRelease APK: 102 location classes survive R8.

- #1061 AR rendering quality umbrella — all P0/P1 sub-issues closed:
  - #1062 baseline-relative light apply (ARScene.kt line 817, AtomicReference)
  - #1063 neutral IBL fallback (createAREnvironment loads neutral_ibl.ktx)
  - #1064 HDR specular filter default true (LightEstimator.kt line 128)
  - #1067 AR exposure aligned to v4.1.0 3D defaults
  - ENVIRONMENTAL_HDR is the default LightEstimationMode (ARScene.kt line 482)
  Remaining sub-issues #1065 (recording resolution) and #1066 (camera-stream
  double-gamma) stay open as P1 polish for v4.4.

Build verification:
- ./gradlew :sceneview:compileReleaseKotlin :arsceneview:compileReleaseKotlin → BUILD SUCCESSFUL
- ./gradlew :samples:android-demo:testDebugUnitTest :arsceneview:testDebugUnitTest → BUILD SUCCESSFUL
- ./gradlew :samples:android-demo:assembleRelease → BUILD SUCCESSFUL (R8 + minify + shrinkResources)
- 30+ version locations bumped 4.3.2 → 4.3.3 (sync-versions.sh: 0 errors / 0 warnings).
- bash .claude/scripts/impact-check.sh → PASS (1 unrelated cross-platform warning).

Co-authored-by: Thomas Gorisse <thomas.gorisse@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thomas-gorisse added a commit that referenced this pull request May 15, 2026
…-14 batch (#1120) (#1342)

Three PRs from yesterday's 15-PR rendering batch shipped without
JVM regression-pin tests. Add focused pins for each:

- #1086 — `environmentalHdrSpecularFilter = true` default. Source pin
  so a future "default-off for perf" PR fails the build instead of
  silently reverting it.
- #1091 — cubemap upload callback. The original double-close fix
  evolved (CORR-B #1094 + #1102 hoist) into the instance-scoped
  `uploadCompletedCallback`. Pin the current contract: callback is
  the hoisted ref, body is a pure `uploadInFlight = false` flip, and
  it never closes `arImages` again (the #1090 double-close).
- #1095 — reflection check that all 7 `LightEstimator` cross-thread
  toggles carry `@Volatile`.

#1088 (ARDefaultCameraNode EV11.6) is already pinned by the existing
`ARDefaultCameraNodeTest.kt`, so no new test was needed there.

Test-only change — no production code touched.

Closes #1120

Co-authored-by: Thomas Gorisse <thomas.gorisse@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AR: IBL reflections cubemap is unfiltered (environmentalHdrSpecularFilter = false default)

1 participant