fix(app-rules): INV-4.6 — UNORM swapchains are linear since #1589; lint the _SRGB choice per leg - #1760
Merged
Conversation
… and the linter checks it per leg The INV-4.6 guide text still described the pre-#1589 model: "a linear / UNORM swapchain is NOT color-managed ... the compositor passes the bytes straight through". Since the format-honest colour model shipped (D3D11 v2.21.0, D3D12 v2.21.1, GL v2.21.2, vk_native v2.21.7, 748b540) an UNORM swapchain is read as LINEAR, as OpenXR specifies, and sRGB-encoded on the way to the panel. An app that stores display-referred bytes in UNORM is encoded twice and looks washed out. The runtime is right; the guide told app authors the opposite. That is what reached the Leia tablet: the Gaussian-splat demo's Android leg prefers {R8G8B8A8_UNORM, B8G8R8A8_UNORM} (unchanged since the leg was written) while its macOS/Linux legs had moved to _SRGB, so the demo was correct on macOS and washed out on Android once the tablet took runtime v2.21.11. Reproduced on macOS vk_native with the Android leg's choice forced: authored (229,182,127) reaches the atlas as (243,220,187) — the exact sRGB encode of the authored value — and v2.20.1 with the same UNORM choice is byte-identical to v2.21.11 with _SRGB. The linter did not catch it because INV-4.6 was app-wide: "an sRGB token appears anywhere" was satisfied by the desktop legs. It is now per leg (shared code counts toward every leg), and in any file that enumerates swapchain formats it flags the two UNORM-first shapes seen in the demos: a preference list naming only UNORM formats, and a scan loop that breaks on the first UNORM. Across the in-tree test_apps the INV-4.6 findings are unchanged; across the five demo repos it flags every Android leg plus the avatar desktop legs. .claude/ (agent worktrees) is now excluded from the scan. scripts/tests/test_check_displayxr_app_color.py pins it (8 hermetic cases, wired into lint.yml); 3 of them fail against the previous linter. The guide also records the on-device A/B for this class: `adb shell setprop debug.xrt.DXR_COLOR_LEGACY_UNORM_ENCODED 1`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chooses the format A leg that creates swapchains in a format negotiated elsewhere (displayxr- common's session code; avatar's windows zone swapchain reuses the main format) was newly warned. It falls back to the app-wide test the check always had; a leg that enumerates formats is still judged on its own. Linted all five demos: every current main is flagged on the UNORM-first legs, and all five fix branches (gauss #138, avatar #113, earthview #76, modelviewer #154, mediaplayer #89) are INV-4.6-clean. test_apps unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
dfattal
added a commit
that referenced
this pull request
Sep 29, 2026
…ormat (#1761) One table for what #1589/#1610 shipped across D3D11, the D3D11 service, D3D12, GL, vk_native (Windows / Linux / macOS-MoltenVK / Android) and Metal: - what each backend enumerates, in order; - what it does with an _SRGB vs a UNORM/float swapchain; - where layers blend; - what the DP receives; - which CTS test pins each row. It also records the rows that are NOT CTS-covered (vk_native on macOS and Android, Metal) and what guards them instead. The app rule fits in one line: write encoded colour into _SRGB, or linear into UNORM; prefer _SRGB, the same way on every leg. Amends ADR-021 (principles unchanged). Its "Model A" now reads as "the atlas is encoded", not "UNORM passes through". Pointers added from ADR-021, INV-4.6 and compositor-pipeline.md. Open rows are recorded, not fixed: - Metal is still pre-#1589 passthrough; - the IPC comp_multi paths are unaudited; - the displayxr-common Windows HUD swapchain is UNORM with display-referred bytes. Motivated by the 2026-09-28 washed-out demos: every demo's Android leg, plus avatar's macOS/Linux legs, picked UNORM and wrote display-referred bytes. Fixed app-side in gauss#138, avatar#113, earthview#76, modelviewer#154 and mediaplayer#89. The per-leg lint is in #1760. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The Gaussian-splat demo looks washed out on the Leia Android tablet running runtime v2.21.11 but looks right on macOS.
The runtime is right. The app, and this repo's app guide, were wrong.
{R8G8B8A8_UNORM, B8G8R8A8_UNORM}and stores display-referred bytes in it, so those bytes are now encoded twice. Its macOS and Linux legs had already moved to_SRGB, which is why the Mac looked right.docs/guides/displayxr-app-rules.mdstill said the old thing: "a linear / UNORM swapchain is NOT color-managed … passes the bytes straight through".check_displayxr_app.pycouldn't catch it: INV-4.6 passed if an sRGB token appeared anywhere in the app, and the desktop legs supplied one.The app fix is in the demo repo: DisplayXR/displayxr-demo-gaussiansplat (linked below).
What this changes
Guide (INV-4.6):
_SRGBholds encoded colour; UNORM is linear and gets encoded, so display-referred bytes in UNORM wash out._SRGB-first choice.adb shell setprop debug.xrt.DXR_COLOR_LEGACY_UNORM_ENCODED 1.Linter, INV-4.6 is now checked per leg. Shared (non-platform) code counts toward every leg. In any file that calls
xrEnumerateSwapchainFormatsit WARNs on:breaks on the first UNORM.The "no sRGB format anywhere" warning is now issued per leg.
.claude/(agent worktrees) is no longer scanned.Tests:
scripts/tests/test_check_displayxr_app_color.pyhas 8 hermetic cases and is wired intolint.yml. 3 of them fail against the previous linter (mutation check).Evidence
Setup: macOS M1 Pro, MoltenVK, vk_native, sim_display. Gauss demo using
GsAdrenoRenderer, the same renderer the Android leg uses, with the Android leg's UNORM choice forced. Values are atlas-capture pixels:_SRGBDXR_COLOR_LEGACY_UNORM_ENCODED=1Linter results
test_apps: INV-4.6 findings are unchanged, old vs new, on every app.Not in this PR: the runtime itself. It stays spec-correct, and CTS depends on it.
🤖 Generated with Claude Code