Skip to content

fix(overlay): render the HUD on SteamVR/OpenXR (sRGB swapchains)#77

Merged
mledour merged 4 commits into
mainfrom
fix/overlay-swapchain-format-fallback
Jul 1, 2026
Merged

fix(overlay): render the HUD on SteamVR/OpenXR (sRGB swapchains)#77
mledour merged 4 commits into
mainfrom
fix/overlay-swapchain-format-fallback

Conversation

@mledour

@mledour mledour commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

The telemetry HUD overlay didn't work on SteamVR/OpenXR (seen via OpenComposite). This branch makes it render there while leaving the passthrough runtimes (Pimax / WMR / Oculus) byte-for-byte unchanged.

What was broken

  • Overlay refused to initialise. pickSwapchainFormat() required DXGI_FORMAT_B8G8R8A8_UNORM exactly and bailed when the runtime didn't advertise it — SteamVR advertises the RGBA8 / sRGB variants instead, so the overlay silently disabled itself.
  • Once it initialised, colours were wrong (washed out): SteamVR only offers _SRGB swapchains and returns a TYPELESS resource; painting through an sRGB RTV double-encoded our already-sRGB UI colours.

Changes

  • Accept a prioritised format listBGRA8_UNORM → RGBA8_UNORM → their sRGB siblings. The GPU shader path writes a logical float4(r,g,b,a) and the output-merger swizzles to the RTV layout, so BGRA8 and RGBA8 render identical colours. On failure we now log the full advertised list instead of guessing.
  • Paint sRGB swapchains through a UNORM RTV so the shader output is stored verbatim and the runtime does the correct sRGB decode at composite — fixes the SteamVR washout. Pimax/WMR/Oculus map to themselves and keep their exact original path.
  • Text coverage-gamma direction follows the composite space via an srgbComposite cbuffer flag threaded to the glyph renderer (see overlay_text_ps.hlsl's DIRECTION CAVEAT): linear composite lifts with 1/TEXT_GAMMA, sRGB composite uses the inverse.
  • Refactor / hardening — single source-of-truth format table, a shared per-image RTV helper with a typed-sRGB fallback, formats.resize(count) on the diagnostic path, and refreshed comments. Addresses the code-review findings raised on the branch.

Testing

  • CI green — build + unit/integration + the overlay snapshot golden, on x64 Debug & Release.
  • On-headset — validated on Pimax OpenXR (unchanged) and SteamVR/OpenXR (overlay renders, colours correct).

Notes / deliberately out of scope

A fully colour-managed path (shaders emit linear on sRGB runtimes + sRGB RTV) was prototyped but reverted: it also changed the look on the passthrough runtimes, which we want left alone. The residual "slightly more open" look of the intentionally-translucent elements (the 0.94 frame fill, the faint grid/axis lines) on SteamVR is inherent to correct linear compositing vs the passthrough runtimes' non-linear compositing — no swapchain-format choice changes it, only per-element alpha. Parked as possible future work; the srgbComposite flag is already plumbed to the text path if we revisit.

🤖 Generated with Claude Code

mledour and others added 4 commits June 30, 2026 15:47
The overlay refused to initialise on SteamVR/OpenXR (seen via
OpenComposite) because pickSwapchainFormat() required
DXGI_FORMAT_B8G8R8A8_UNORM exactly and bailed when the runtime didn't
advertise it. SteamVR advertises RGBA8/sRGB variants instead.

Accept a prioritised list (BGRA8_UNORM -> RGBA8_UNORM -> their sRGB
siblings) and create the per-image RTV in the chosen format on both the
D3D11 and D3D11On12 paths. The GPU shader path writes a logical
float4(r,g,b,a) and the output-merger swizzles to the RTV layout, so an
RGBA8 target renders identical colours. On failure we now log the full
list of formats the runtime advertised instead of guessing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…out)

SteamVR/OpenXR advertises only the _SRGB swapchain variants (no linear
UNORM) and returns a TYPELESS resource. The previous change picked
B8G8R8A8_UNORM_SRGB and created the RTV in that same sRGB format, so the
GPU applied linear->sRGB encoding when writing our already-sRGB UI
colours; SteamVR then decoded again, leaving the overlay washed out /
light grey. Pimax/WMR/Oculus were unaffected because they advertise a
UNORM format.

Add rtvFormatForSwapchain(): map an sRGB swapchain pick to its UNORM
sibling (91->87, 29->28) for the render-target view, so the shader output
is stored verbatim and the runtime does the correct sRGB decode on
composite. A UNORM view over the TYPELESS resource is valid. UNORM picks
pass through unchanged, so the working runtimes keep their exact path.
Applied to both the D3D11 and D3D11On12 backends.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…path

Addresses code-review findings on the swapchain-format fallback:

- Single source of truth: fold the accept list and the sRGB->UNORM RTV
  map into one kOverlayFormats table, so adding a format is one row and
  the two can't drift out of sync (a missing map entry previously fell
  through to an sRGB RTV, silently reintroducing the washout).
- RTV fallback: createOverlayImageRtv retries with the sRGB view if the
  UNORM view is rejected, for runtimes that hand back a fully-typed
  (non-typeless) sRGB resource where CreateRenderTargetView(UNORM) would
  fail with E_INVALIDARG. Shared by the D3D11 and D3D11On12 paths.
- Diagnostics: the 'renderer ready' logs now record the picked swapchain
  format (and tag it (sRGB)), not just the de-sRGB'd RTV format, so a
  color/text report shows whether the sRGB composite path engaged.
- Fix the failure-path format list: trust the count from the second
  xrEnumerateSwapchainFormats (resize) so the diagnostic never lists
  stale trailing zeros or a count that disagrees with the list.
- Refresh stale comments that still claimed the RTV is always BGRA8_UNORM.

No behavioural change on the working runtimes (UNORM picks map to
themselves); SteamVR's sRGB pick keeps the UNORM RTV from c95bb97.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… space

The glyph PS pre-corrects DirectWrite coverage with pow(c, 1/TEXT_GAMMA),
tuned for runtimes that composite the quad in linear space (UNORM
swapchain). Now that the overlay also runs on sRGB-decoding runtimes
(SteamVR), that direction is wrong there — the shader's own DIRECTION
CAVEAT says to invert it — so HUD text renders at the wrong stem weight.

Thread an `srgbComposite` flag from the picked swapchain format through
initOverlayRenderers -> glyph Renderer::init -> the TextConstants cbuffer
(reusing a pad slot, layout unchanged, static_assert intact). The PS
picks pow(c, 1/TEXT_GAMMA) for linear composites and pow(c, TEXT_GAMMA)
for sRGB. Gated on supersample > 1, so the snapshot/golden (1x) path is
byte-identical; the flag defaults to false so every other init() caller
keeps the linear direction.

Direction + weight are still worth an on-headset A/B (SteamVR vs Pimax);
tunable via TEXT_GAMMA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Overlay snapshot test now passing

The overlay snapshot test passes on this PR.

@mledour mledour force-pushed the fix/overlay-swapchain-format-fallback branch 2 times, most recently from f236b2c to a4d2aea Compare July 1, 2026 10:08
@mledour mledour changed the title fix(overlay): accept RGBA8/sRGB swapchain formats, not just BGRA8 fix(overlay): render the HUD on SteamVR/OpenXR (sRGB swapchains) Jul 1, 2026
@mledour mledour merged commit 061cbfd into main Jul 1, 2026
6 checks passed
@mledour mledour deleted the fix/overlay-swapchain-format-fallback branch July 1, 2026 12:17
github-actions Bot added a commit that referenced this pull request Jul 1, 2026
@mledour mledour mentioned this pull request Jul 1, 2026
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.

1 participant