Skip to content

fix(#347): _SRGB swapchains on format-honest runtimes — colour no longer washed out - #348

Merged
dfattal merged 2 commits into
mainfrom
fix/347-srgb-swapchains
Oct 2, 2026
Merged

dfattal merged 2 commits into
mainfrom
fix/347-srgb-swapchains

Conversation

@dfattal

@dfattal dfattal commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #347. On runtime ≥ v2.21 every Gamma-project Unity app, and every window-space UI / Local2D canvas, looked washed out on the panel. Seen on the Windows Leia panel with lenovo-avatar.

The runtime is right. Since v2.21.0 it follows OpenXR's format rule, which the CTS checks (ADR-044, INV-4.6): an _SRGB swapchain holds encoded colour, and a UNORM one holds linear values that the runtime encodes. The plugin was putting encoded bytes into UNORM swapchains, so they were encoded twice.

What changes

When it applies: only on a format-honest runtime. That is runtimeVersion (from xrGetInstanceProperties) ≥ 2.21.0 on D3D11, 2.21.1 on D3D12, or 2.21.7 on Vulkan, and the runtime's own DXR_COLOR_LEGACY_UNORM_ENCODED=1 hatch is not set.

Layer Before After
Primary, Linear project _SRGB, Unity encodes on store unchanged
Primary, Gamma project UNORM ← encoded bytes ✗ _SRGB swapchain. Unity renders into the UNORM sibling (s_unity_format), and the existing raw copy (CopyResource / CopyTextureRegion / vkCmdCopyImage) moves the bytes in unchanged
wsui / Local2D (D3D11, D3D12, Vulkan) UNORM ← encoded canvas bytes ✗ _SRGB sibling. Bridges stay UNORM; the copies are raw
Extra 3D zones (D3D), Gamma UNORM ✗ _SRGB sibling (Linear projects are unchanged; their zone targets aren't sRGB-flagged)
Metal / older runtimes — byte-identical to before (Metal isn't format-honest yet, ADR-044 §7)

dxr_prov_swapchain_is_srgb(), which drives kUnityXRRenderTextureFlagsSRGB, now means "Unity encodes on store". It no longer means "the swapchain is sRGB": a Gamma project has an _SRGB swapchain but must not encode.

This PR also refreshes the Windows x64 DLL. The committed one predated #337/#339/#341.

Testing

Windows, Leia panel, installed runtime v2.22.0, lenovo-avatar (Gamma, URP) player with this DLL, D3D12:

[DisplayXR-PROV] runtime: DisplayXR Runtime (based on Monado/XRT) 'v2.22.0' 2.22.0
[DisplayXR-PROV] swapchain format=29 srgb=1 unity_format=28 unity_encodes=0 (linear=0 present_path=1 format_honest=1)
[DisplayXR-PROV] wsui: swapchain 1412x820 (3 imgs, fmt=91) ... (D3D12 bridge)
[DisplayXR-PROV] wsui: swapchain 1074x650 (3 imgs, fmt=91) ... (D3D12 bridge)

No xrEndFrame errors and no WARN lines.

Still to do before merge:

  • Eyeball on the panel (David)
  • D3D11 run
  • A Linear project (hdrp-singlepass-ui)
  • Linux/Vulkan (Byungju)
  • Atlas captures against authored colour (ADR-044 §5)

Not in scope (app side, from the ADR-044 audit): lenovo's separate native Vulkan app (native-vk~) creates its own UNORM swapchains and needs the displayxr-common ≥ v2.15.0 bump plus _SRGB window-space swapchains.

Known residual: a Gamma project blends its canvas in encoded space, while the runtime now blends layers in linear light, so semi-transparent HUD pixels come out slightly lighter. Opaque UI is exact.

🤖 Generated with Claude Code

…ur isn't washed out

Since runtime v2.21.0 (D3D11), v2.21.1 (D3D12) and v2.21.7 (Vulkan) the runtime
believes the swapchain format (ADR-044 / INV-4.6, pinned by the Khronos CTS): an
_SRGB swapchain holds encoded colour, and a UNORM one holds LINEAR values that it
sRGB-encodes on the way to the panel. The plugin put encoded bytes into UNORM
swapchains (the primary layer of every Gamma project, plus every wsui/Local2D canvas
and the Gamma extra zones), so they were encoded twice and looked washed out.

On a format-honest runtime (version from xrGetInstanceProperties, per graphics API;
off under the runtime's own DXR_COLOR_LEGACY_UNORM_ENCODED=1 hatch):
- Primary: _SRGB in both colour spaces. What Unity renders into is split from the
  swapchain format: a Linear project keeps encoding on store; a Gamma project renders
  into the UNORM sibling and the existing raw copy (CopyResource / CopyTextureRegion /
  vkCmdCopyImage) moves the encoded bytes into the _SRGB image unchanged.
- wsui / Local2D (D3D11, D3D12, Vulkan): the _SRGB sibling; bridges stay UNORM.
- Extra 3D zones (D3D, Gamma projects): the _SRGB sibling.
Metal is not format-honest yet (ADR-044 section 7) and older runtimes keep the
previous formats, byte for byte.

Also refreshes the Windows x64 DLL, which predated #337/#339/#341.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@byungjul

byungjul commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Linux/Vulkan results on the DS1 (Ubuntu 26.04, RTX 4090 Laptop, lenovo-avatar, installed runtime v2.21.11):

As-is, #348 doesn't engage on Linux. The Linux runtime reports runtimeVersion = 1.6.0, from the installed .deb and from a local build alike, so ps_runtime_format_honest() returns 0:

[DisplayXR-PROV] runtime: DisplayXR Runtime (based on Monado/XRT) 'v2.21.11' 1.6.0
[DisplayXR-PROV] swapchain format=37 srgb=0 unity_format=37 unity_encodes=0 (... format_honest=0)

The runtime itself is format-honest (Color (#1589) [vk_native]: format-honest ...), so on Linux, main and #348 both stay washed out. Cause: the Linux workflow never syncs CMake VERSION from the tag. Filed as DisplayXR/displayxr-runtime#1790.

With the version read correctly, the fix works. For the test I added a fallback that parses the 'vX.Y.Z' tag from runtimeName when runtimeVersion major is < 2:

[DisplayXR-PROV] runtime: DisplayXR Runtime (based on Monado/XRT) 'v2.21.11' 2.21.11
[DisplayXR-PROV] swapchain format=43 srgb=1 unity_format=37 unity_encodes=0 (linear=0 present_path=1 format_honest=1)
[DisplayXR-PROV-VK] swapchain images: 3 x (840x1086 arr=2 fmt=43)
[DisplayXR-PROV] local2d: Vulkan swapchain 1485x640 (3 imgs, fmt=50) + overlay bridge
[DisplayXR-PROV] wsui: Vulkan swapchain 800x820 (3 imgs, fmt=50) + overlay bridge
[DisplayXR-PROV] wsui: Vulkan swapchain 1024x650 (3 imgs, fmt=50) + overlay bridge
  • No xrEndFrame failures, no validation errors, no Vulkan errors. The bridge copies are raw (src fmt=44 -> bridge fmt=44, copy).
  • By eye: the colour is clearly deeper than main, and the blacks are no longer lifted.

Suggestion: keep a fallback like this in #348. Every Linux runtime shipped so far reports 1.6.0, so even with #1790 fixed, apps on today's .deb would stay washed out without it. The test patch, in the runtime-version block:

// Linux runtimes report runtimeVersion 1.6.0 (displayxr-runtime#1790): fall back to
// the 'vX.Y.Z' tag in runtimeName.
if (XR_VERSION_MAJOR(s_runtime_version) < 2) {
	const char *q = strstr(ip.runtimeName, "'v");
	unsigned a = 0, b = 0, c = 0;
	if (q && sscanf(q + 2, "%u.%u.%u", &a, &b, &c) == 3)
		s_runtime_version = XR_MAKE_VERSION(a, b, c);
}

🤖 Generated with Claude Code

…ion is 1.x (Linux)

Every Linux runtime shipped so far reports runtimeVersion 1.6.0
(DisplayXR/displayxr-runtime#1790), so the format-honest gate never engaged on Linux
and apps stayed washed out. Fall back to the 'vX.Y.Z' tag in runtimeName when the
major is < 2. Verified by Byungju on the DS1 (Ubuntu, RTX 4090, runtime v2.21.11):
_SRGB swapchains (43 primary, 50 wsui/Local2D), raw bridge copies, no errors, and
correct colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal

dfattal commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks @byungjul, great catch. Your runtimeName fallback is in ea0ac95 as you wrote it, plus an a >= 2 sanity check so a malformed tag can't lower the gate. Windows DLL rebuilt. Your DS1 log (format=43 … format_honest=1, fmt=50 on wsui/Local2D, raw bridge copies, colour deeper by eye) counts as the Linux verification for this PR.

@dfattal
dfattal marked this pull request as ready for review October 2, 2026 08:58
@dfattal
dfattal merged commit 91787b4 into main Oct 2, 2026
8 checks passed
@dfattal
dfattal deleted the fix/347-srgb-swapchains branch October 2, 2026 08:58
dfattal added a commit that referenced this pull request Oct 2, 2026
…from the merged source

Co-Authored-By: Claude Opus 5.5 <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.

Washed-out colour on runtime ≥ v2.21: UNORM swapchains carry encoded bytes (ADR-044 / INV-4.6)

2 participants